@yemi33/minions 0.1.1648 → 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 +10 -0
- package/dashboard/js/render-prs.js +2 -1
- package/engine/ado-status.js +5 -1
- package/engine/ado.js +261 -102
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +136 -1
- package/engine/timeout.js +17 -4
- package/engine.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1650 (2026-05-01)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- reject premature task_complete for nonterminal summaries
|
|
7
|
+
- ADO build poll repositoryId GUID handling
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
- yemi33/minions#1927
|
|
11
|
+
- yemi33/minions#1925
|
|
12
|
+
|
|
3
13
|
## 0.1.1648 (2026-05-01)
|
|
4
14
|
|
|
5
15
|
### Other
|
|
@@ -16,6 +16,7 @@ function prRow(pr) {
|
|
|
16
16
|
const reviewTitle = reviewEscalated ? 'Review/re-review and review-fix automation stopped after evalMaxIterations; build-fix and conflict-fix automation may still run.' : '';
|
|
17
17
|
const buildClass = pr.buildFixEscalated ? 'build-escalated' : pr._buildStatusStale ? 'build-stale' : pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
18
18
|
const buildLabel = pr.buildFixEscalated ? 'escalated (' + (pr.buildFixAttempts || '?') + ' fixes)' : (pr.buildStatus || 'none') + (pr._buildStatusStale ? ' (stale)' : '');
|
|
19
|
+
const buildTitle = pr._buildStatusDetail || '';
|
|
19
20
|
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
20
21
|
const statusLabel = pr.status || 'active';
|
|
21
22
|
const branchError = pr._branchResolutionError?.reason || '';
|
|
@@ -34,7 +35,7 @@ function prRow(pr) {
|
|
|
34
35
|
'<td><span class="' + branchClass + '"' + (branchError ? ' title="' + escapeHtml(branchError) + '"' : '') + '>' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
|
|
35
36
|
'<td><span class="pr-badge ' + reviewClass + '"' + (reviewTitle ? ' title="' + escapeHtml(reviewTitle) + '"' : '') + '>' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
36
37
|
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escapeHtml(sq.note || '') + '">' + escapeHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>' : pr.reviewedBy && pr.reviewedBy.length ? '<span class="pr-agent">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
37
|
-
'<td><span class="pr-badge ' + buildClass + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
|
38
|
+
'<td><span class="pr-badge ' + buildClass + '"' + (buildTitle ? ' title="' + escapeHtml(buildTitle) + '"' : '') + '>' + escapeHtml(buildLabel) + '</span></td>' +
|
|
38
39
|
'<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
39
40
|
'<td><span class="pr-date">' + escapeHtml((pr.created || '—').slice(0, 16).replace('T', ' ')) + '</span></td>' +
|
|
40
41
|
'<td><button class="pr-pager-btn" style="font-size:9px;padding:1px 5px;color:var(--red);border-color:var(--red)" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
|
package/engine/ado-status.js
CHANGED
|
@@ -42,7 +42,7 @@ if (!prNumberArg) {
|
|
|
42
42
|
' --project <name> Scope search to one project (optional)',
|
|
43
43
|
'',
|
|
44
44
|
'Output: JSON with fields: prNumber, title, branch, status, reviewStatus,',
|
|
45
|
-
' buildStatus, buildErrorLog (if failing), mergeConflict, url, project, source',
|
|
45
|
+
' buildStatus, buildStatusStale/detail (if stale), buildErrorLog (if failing), mergeConflict, url, project, source',
|
|
46
46
|
'',
|
|
47
47
|
'buildStatus values: passing | failing | running | none',
|
|
48
48
|
'reviewStatus values: approved | changes-requested | waiting | pending',
|
|
@@ -73,6 +73,10 @@ function findInCache(projects) {
|
|
|
73
73
|
source: 'cached',
|
|
74
74
|
};
|
|
75
75
|
if (pr.buildErrorLog) out.buildErrorLog = pr.buildErrorLog;
|
|
76
|
+
if (pr._buildStatusStale) {
|
|
77
|
+
out.buildStatusStale = true;
|
|
78
|
+
if (pr._buildStatusDetail) out.buildStatusDetail = pr._buildStatusDetail;
|
|
79
|
+
}
|
|
76
80
|
if (pr._mergeConflict) out.mergeConflict = true;
|
|
77
81
|
return out;
|
|
78
82
|
}
|
package/engine/ado.js
CHANGED
|
@@ -26,17 +26,31 @@ const getAdoPrUrl = (project, prNumber) => {
|
|
|
26
26
|
const repoPath = encodeURIComponent(project.repoName || project.repositoryId || '');
|
|
27
27
|
return `https://dev.azure.com/${project.adoOrg}/${project.adoProject}/_git/${repoPath}/pullrequest/${prNumber}`;
|
|
28
28
|
};
|
|
29
|
+
const ADO_GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
29
30
|
|
|
30
31
|
function isGitHubProject(project) {
|
|
31
32
|
return String(project?.repoHost || '').toLowerCase() === 'github';
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function
|
|
35
|
+
function isAdoGuid(value) {
|
|
36
|
+
return ADO_GUID_RE.test(String(value || '').trim());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getAdoRepositoryLookupKey(project) {
|
|
35
40
|
const repositoryId = String(project?.repositoryId || '').trim();
|
|
36
41
|
if (repositoryId) return repositoryId;
|
|
37
42
|
return String(project?.repoName || '').trim();
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
function getAdoRepositoryId(project) {
|
|
46
|
+
return getAdoRepositoryLookupKey(project);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getConfiguredAdoRepositoryGuid(project) {
|
|
50
|
+
const repositoryId = String(project?.repositoryId || '').trim();
|
|
51
|
+
return isAdoGuid(repositoryId) ? repositoryId : '';
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
function getAdoProjectLabel(project) {
|
|
41
55
|
return project?.name || project?.repoName || `${project?.adoOrg || 'unknown-org'}/${project?.adoProject || 'unknown-project'}`;
|
|
42
56
|
}
|
|
@@ -45,6 +59,85 @@ function logMissingAdoRepository(project, purpose) {
|
|
|
45
59
|
log('error', `${purpose} disabled for project ${getAdoProjectLabel(project)}: missing project.repositoryId and project.repoName; configure one so Azure DevOps repository API calls can target the repo`);
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
function adoConfigPath() {
|
|
63
|
+
return path.join(shared.MINIONS_DIR, 'config.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sameAdoProject(a, b) {
|
|
67
|
+
if (!a || !b) return false;
|
|
68
|
+
if (a.name && b.name && a.name === b.name) return true;
|
|
69
|
+
if (a.localPath && b.localPath && path.resolve(a.localPath) === path.resolve(b.localPath)) return true;
|
|
70
|
+
return String(a.adoOrg || '') === String(b.adoOrg || '')
|
|
71
|
+
&& String(a.adoProject || '') === String(b.adoProject || '')
|
|
72
|
+
&& String(a.repoName || '') === String(b.repoName || '')
|
|
73
|
+
&& String(a.repoHost || 'ado').toLowerCase() === String(b.repoHost || 'ado').toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function persistAdoRepositoryGuid(project, guid, repoName) {
|
|
77
|
+
if (!isAdoGuid(guid)) return;
|
|
78
|
+
const previous = String(project?.repositoryId || '').trim();
|
|
79
|
+
project.repositoryId = guid;
|
|
80
|
+
if (!project.repoName && repoName) project.repoName = repoName;
|
|
81
|
+
if (previous === guid) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
let persisted = false;
|
|
85
|
+
mutateJsonFileLocked(adoConfigPath(), (config) => {
|
|
86
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
|
|
87
|
+
if (!Array.isArray(config.projects)) return config;
|
|
88
|
+
const target = config.projects.find(p => sameAdoProject(p, project));
|
|
89
|
+
if (!target) return config;
|
|
90
|
+
target.repositoryId = guid;
|
|
91
|
+
if (!target.repoName && repoName) target.repoName = repoName;
|
|
92
|
+
persisted = true;
|
|
93
|
+
return config;
|
|
94
|
+
}, { defaultValue: { projects: [] }, skipWriteIfUnchanged: true });
|
|
95
|
+
if (persisted) {
|
|
96
|
+
log('info', `Resolved ADO repository GUID for ${getAdoProjectLabel(project)}: ${previous || project.repoName || 'unknown'} → ${guid}`);
|
|
97
|
+
} else {
|
|
98
|
+
log('warn', `Resolved ADO repository GUID for ${getAdoProjectLabel(project)} but could not find the project in config.json to persist it`);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
log('warn', `Resolved ADO repository GUID for ${getAdoProjectLabel(project)} but failed to persist it: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function resolveAdoBuildRepositoryGuid(project, token, orgBase, purpose, opts = {}) {
|
|
106
|
+
const configured = getConfiguredAdoRepositoryGuid(project);
|
|
107
|
+
if (configured) return configured;
|
|
108
|
+
|
|
109
|
+
const lookupKey = getAdoRepositoryLookupKey(project);
|
|
110
|
+
if (!lookupKey) {
|
|
111
|
+
logMissingAdoRepository(project, purpose);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const repoUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodeURIComponent(lookupKey)}?api-version=7.1`;
|
|
117
|
+
const repoData = await adoFetch(repoUrl, token, opts);
|
|
118
|
+
const guid = String(repoData?.id || '').trim();
|
|
119
|
+
if (!isAdoGuid(guid)) {
|
|
120
|
+
log('error', `${purpose} disabled for project ${getAdoProjectLabel(project)}: ADO repository lookup for "${lookupKey}" did not return a repository GUID`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
persistAdoRepositoryGuid(project, guid, repoData?.name || project.repoName || '');
|
|
124
|
+
return guid;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
log('warn', `${purpose} could not resolve repository GUID for project ${getAdoProjectLabel(project)} from "${lookupKey}": ${e.message}`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function markBuildStatusStale(pr, detail) {
|
|
132
|
+
pr._buildStatusStale = true;
|
|
133
|
+
if (detail) pr._buildStatusDetail = detail;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function clearBuildStatusStale(pr) {
|
|
137
|
+
if (pr._buildStatusStale) delete pr._buildStatusStale;
|
|
138
|
+
if (pr._buildStatusDetail) delete pr._buildStatusDetail;
|
|
139
|
+
}
|
|
140
|
+
|
|
48
141
|
// ── Build/Review Status Helpers ───────────────────────────────────────────────
|
|
49
142
|
|
|
50
143
|
/** Classify an array of ADO build records into a single status string. */
|
|
@@ -353,9 +446,6 @@ async function pollPrStatus(config) {
|
|
|
353
446
|
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}/pullrequests/${prNum}`;
|
|
354
447
|
let updated = false;
|
|
355
448
|
|
|
356
|
-
// Clear stale flag — we're attempting a fresh poll
|
|
357
|
-
if (pr._buildStatusStale) { delete pr._buildStatusStale; updated = true; }
|
|
358
|
-
|
|
359
449
|
const prData = await adoFetch(`${repoBase}?api-version=7.1`, token);
|
|
360
450
|
|
|
361
451
|
const sourceBranch = stripRefsHeads(prData.sourceRefName);
|
|
@@ -387,6 +477,8 @@ async function pollPrStatus(config) {
|
|
|
387
477
|
delete pr.buildFailReason;
|
|
388
478
|
delete pr.buildErrorLog;
|
|
389
479
|
delete pr._buildFailNotified;
|
|
480
|
+
delete pr._buildStatusStale;
|
|
481
|
+
delete pr._buildStatusDetail;
|
|
390
482
|
delete pr.buildFixAttempts;
|
|
391
483
|
delete pr.buildFixEscalated;
|
|
392
484
|
}
|
|
@@ -485,42 +577,59 @@ async function pollPrStatus(config) {
|
|
|
485
577
|
// merge commit (same ref accumulates builds across all prior pushes to the PR).
|
|
486
578
|
const prNumber = pr.prNumber;
|
|
487
579
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
488
|
-
let buildStatus = 'none';
|
|
489
|
-
let buildFailReason = '';
|
|
580
|
+
let buildStatus = pr.buildStatus || 'none';
|
|
581
|
+
let buildFailReason = pr.buildFailReason || '';
|
|
490
582
|
let buildStatuses = []; // for error log fetching
|
|
583
|
+
let buildStatusResolved = true;
|
|
584
|
+
let buildStatusStaleDetail = '';
|
|
491
585
|
|
|
492
586
|
if (prNumber && mergeCommitId) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
587
|
+
const buildRepositoryGuid = await resolveAdoBuildRepositoryGuid(project, token, orgBase, 'ADO build polling');
|
|
588
|
+
if (!buildRepositoryGuid) {
|
|
589
|
+
buildStatusResolved = false;
|
|
590
|
+
buildStatusStaleDetail = 'ADO Builds API requires a repository GUID; repository GUID could not be resolved from project.repositoryId/project.repoName';
|
|
591
|
+
} else {
|
|
592
|
+
try {
|
|
593
|
+
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
594
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodeURIComponent(buildRepositoryGuid)}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
595
|
+
const buildsData = await adoFetch(buildsUrl, token);
|
|
596
|
+
const allBuilds = buildsData?.value || [];
|
|
597
|
+
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
598
|
+
buildStatus = 'none';
|
|
599
|
+
buildFailReason = '';
|
|
600
|
+
|
|
601
|
+
if (prBuilds.length > 0) {
|
|
602
|
+
buildStatus = classifyBuildStatus(prBuilds);
|
|
603
|
+
if (buildStatus === 'failing') {
|
|
604
|
+
const failed = prBuilds.find(b => b.result === 'failed');
|
|
605
|
+
buildFailReason = failed?.definition?.name || 'Build failed';
|
|
606
|
+
// Build fake status objects for error log fetching
|
|
607
|
+
buildStatuses = prBuilds.filter(b => b.result === 'failed').map(b => ({
|
|
608
|
+
state: 'failed', targetUrl: `${orgBase}/${project.adoProject}/_build/results?buildId=${b.id}`,
|
|
609
|
+
_buildId: String(b.id),
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
} else if (allBuilds.length > 0 && pr.buildStatus) {
|
|
613
|
+
// Stale merge-commit fallback — ADO returned builds for this PR's merge ref
|
|
614
|
+
// but none target the current `mergeCommitId`. Most likely the target branch
|
|
615
|
+
// moved, ADO recomputed the merge commit, but no new source-side changes
|
|
616
|
+
// triggered a rebuild. Preserve the previous `pr.buildStatus` so the tracker
|
|
617
|
+
// reflects the last known truth instead of flipping to a spurious 'none'.
|
|
618
|
+
// Also log a warn so stale states are detectable in engine logs. Issue #1233.
|
|
619
|
+
const sampleSv = (allBuilds[0]?.sourceVersion || '').slice(0, 8);
|
|
620
|
+
log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv}); preserving previous buildStatus '${pr.buildStatus}'`);
|
|
621
|
+
buildStatus = pr.buildStatus;
|
|
622
|
+
if (pr.buildFailReason) buildFailReason = pr.buildFailReason;
|
|
510
623
|
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// triggered a rebuild. Preserve the previous `pr.buildStatus` so the tracker
|
|
516
|
-
// reflects the last known truth instead of flipping to a spurious 'none'.
|
|
517
|
-
// Also log a warn so stale states are detectable in engine logs. Issue #1233.
|
|
518
|
-
const sampleSv = (allBuilds[0]?.sourceVersion || '').slice(0, 8);
|
|
519
|
-
log('warn', `PR ${pr.id} build: merge-commit mismatch — ${allBuilds.length} build(s) on merge ref, none target current merge commit ${String(mergeCommitId).slice(0, 8)} (sample sourceVersion ${sampleSv}); preserving previous buildStatus '${pr.buildStatus}'`);
|
|
520
|
-
buildStatus = pr.buildStatus;
|
|
521
|
-
if (pr.buildFailReason) buildFailReason = pr.buildFailReason;
|
|
624
|
+
} catch (e) {
|
|
625
|
+
buildStatusResolved = false;
|
|
626
|
+
buildStatusStaleDetail = `ADO build query failed: ${e.message}`;
|
|
627
|
+
log('warn', `ADO build query for ${pr.id}: ${e.message}; preserving previous buildStatus '${pr.buildStatus || 'none'}'`);
|
|
522
628
|
}
|
|
523
|
-
}
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
buildStatus = 'none';
|
|
632
|
+
buildFailReason = '';
|
|
524
633
|
}
|
|
525
634
|
|
|
526
635
|
// Record actual poll time — makes lastBuildCheck reflect when the engine last
|
|
@@ -528,50 +637,64 @@ async function pollPrStatus(config) {
|
|
|
528
637
|
pr.lastBuildCheck = ts();
|
|
529
638
|
updated = true;
|
|
530
639
|
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
else delete pr.buildFailReason;
|
|
536
|
-
// Build transitioned — clear grace period and auto-complete flag
|
|
537
|
-
delete pr._buildFixPushedAt;
|
|
538
|
-
if (buildStatus === 'failing') delete pr._autoCompleted;
|
|
539
|
-
if (buildStatus !== 'failing') {
|
|
540
|
-
delete pr._buildFailNotified;
|
|
541
|
-
// Preserve buildErrorLog + buildFixAttempts through transient 'none'/'running'
|
|
542
|
-
// transitions — only clear on confirmed 'passing' recovery. Issue #1232: 'none'
|
|
543
|
-
// can also occur when ADO recomputes the merge commit after a target-branch
|
|
544
|
-
// update but no new builds have been triggered yet (filter by sourceVersion
|
|
545
|
-
// returns []), which previously wiped the last known error log and caused
|
|
546
|
-
// fix agents to be dispatched blind.
|
|
547
|
-
if (buildStatus === 'passing') {
|
|
548
|
-
delete pr.buildErrorLog;
|
|
549
|
-
// Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
|
|
550
|
-
if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
|
|
551
|
-
}
|
|
640
|
+
if (buildStatusResolved) {
|
|
641
|
+
if (pr._buildStatusStale || pr._buildStatusDetail) {
|
|
642
|
+
clearBuildStatusStale(pr);
|
|
643
|
+
updated = true;
|
|
552
644
|
}
|
|
645
|
+
} else {
|
|
646
|
+
markBuildStatusStale(pr, buildStatusStaleDetail);
|
|
553
647
|
updated = true;
|
|
648
|
+
}
|
|
554
649
|
|
|
555
|
-
|
|
556
|
-
if (buildStatus
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (
|
|
565
|
-
pr.
|
|
566
|
-
|
|
650
|
+
if (buildStatusResolved) {
|
|
651
|
+
if (pr.buildStatus !== buildStatus) {
|
|
652
|
+
log('info', `PR ${pr.id} build: ${pr.buildStatus || 'none'} → ${buildStatus}${buildFailReason ? ' (' + buildFailReason + ')' : ''}`);
|
|
653
|
+
pr.buildStatus = buildStatus;
|
|
654
|
+
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
655
|
+
else delete pr.buildFailReason;
|
|
656
|
+
// Build transitioned — clear grace period and auto-complete flag
|
|
657
|
+
delete pr._buildFixPushedAt;
|
|
658
|
+
if (buildStatus === 'failing') delete pr._autoCompleted;
|
|
659
|
+
if (buildStatus !== 'failing') {
|
|
660
|
+
delete pr._buildFailNotified;
|
|
661
|
+
delete pr._buildStatusStale;
|
|
662
|
+
delete pr._buildStatusDetail;
|
|
663
|
+
// Preserve buildErrorLog + buildFixAttempts through transient 'none'/'running'
|
|
664
|
+
// transitions — only clear on confirmed 'passing' recovery. Issue #1232: 'none'
|
|
665
|
+
// can also occur when ADO recomputes the merge commit after a target-branch
|
|
666
|
+
// update but no new builds have been triggered yet (filter by sourceVersion
|
|
667
|
+
// returns []), which previously wiped the last known error log and caused
|
|
668
|
+
// fix agents to be dispatched blind.
|
|
669
|
+
if (buildStatus === 'passing') {
|
|
670
|
+
delete pr.buildErrorLog;
|
|
671
|
+
// Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
|
|
672
|
+
if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
|
|
673
|
+
}
|
|
567
674
|
}
|
|
675
|
+
updated = true;
|
|
568
676
|
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
677
|
+
// Fetch actual compiler/build error logs when transitioning to failing
|
|
678
|
+
if (buildStatus === 'failing') {
|
|
679
|
+
const failedStatusObjs = buildStatuses.filter(s => s.state === 'failed' || s.state === 'error').slice(0, 10);
|
|
680
|
+
const logParts = [];
|
|
681
|
+
const seenBuildIds = new Set();
|
|
682
|
+
for (const failedStatusObj of failedStatusObjs) {
|
|
683
|
+
const errorLog = await fetchAdoBuildErrorLog(orgBase, project, failedStatusObj, token, pr, seenBuildIds);
|
|
684
|
+
if (errorLog) logParts.push(errorLog);
|
|
685
|
+
}
|
|
686
|
+
if (logParts.length > 0) {
|
|
687
|
+
pr.buildErrorLog = logParts.join('\n\n');
|
|
688
|
+
log('info', `PR ${pr.id}: fetched error logs from ${logParts.length} failing pipeline(s)`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Teams notification for build failure — non-blocking
|
|
692
|
+
try {
|
|
693
|
+
const teams = require('./teams');
|
|
694
|
+
const prFilePath = shared.projectPrPath(project);
|
|
695
|
+
teams.teamsNotifyPrEvent(pr, 'build-failed', project, prFilePath).catch(() => {});
|
|
696
|
+
} catch {}
|
|
697
|
+
}
|
|
575
698
|
}
|
|
576
699
|
}
|
|
577
700
|
|
|
@@ -618,7 +741,7 @@ async function pollPrStatus(config) {
|
|
|
618
741
|
// Auth errors → mark build status stale so dashboard shows uncertainty
|
|
619
742
|
// and engine re-polls on next tick instead of waiting 6 ticks
|
|
620
743
|
if (isAdoAuthError(err)) {
|
|
621
|
-
pr
|
|
744
|
+
markBuildStatusStale(pr, `ADO auth error: ${err.message}`);
|
|
622
745
|
_adoPollHadAuthFailure = true;
|
|
623
746
|
log('warn', `PR ${pr.id}: build status marked stale (auth error: ${err.message})`);
|
|
624
747
|
return true; // count as updated to persist the stale flag
|
|
@@ -923,6 +1046,8 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
923
1046
|
* {
|
|
924
1047
|
* buildStatus: 'failing' | 'passing' | 'running' | 'none' | null,
|
|
925
1048
|
* mergeConflict: boolean,
|
|
1049
|
+
* buildStatusStale?: boolean,
|
|
1050
|
+
* buildStatusDetail?: string,
|
|
926
1051
|
* }
|
|
927
1052
|
*
|
|
928
1053
|
* `buildStatus` is null when ADO has builds on the merge ref but none target the
|
|
@@ -937,12 +1062,12 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
937
1062
|
const orgBase = shared.getAdoOrgBase(project);
|
|
938
1063
|
const prNum = shared.getPrNumber(pr);
|
|
939
1064
|
if (!prNum) return null;
|
|
940
|
-
const
|
|
941
|
-
if (!
|
|
1065
|
+
const adoRepositoryLookupKey = getAdoRepositoryLookupKey(project);
|
|
1066
|
+
if (!adoRepositoryLookupKey) {
|
|
942
1067
|
logMissingAdoRepository(project, 'ADO live build/conflict check');
|
|
943
1068
|
return null;
|
|
944
1069
|
}
|
|
945
|
-
const encodedRepoId = encodeURIComponent(
|
|
1070
|
+
const encodedRepoId = encodeURIComponent(adoRepositoryLookupKey);
|
|
946
1071
|
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}`;
|
|
947
1072
|
const prUrl = `${repoBase}/pullrequests/${prNum}?api-version=7.1`;
|
|
948
1073
|
// 4s timeout — same budget as checkLiveReviewStatus. This is a pre-dispatch
|
|
@@ -959,23 +1084,35 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
959
1084
|
// pollPrStatus's narrowing logic so the live check and the cached poll
|
|
960
1085
|
// agree on what 'failing' / 'passing' / 'running' / 'none' mean.
|
|
961
1086
|
let buildStatus = null;
|
|
1087
|
+
let buildStatusStale = false;
|
|
1088
|
+
let buildStatusDetail = '';
|
|
962
1089
|
if (prData.status === 'active') {
|
|
963
1090
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
964
1091
|
if (mergeCommitId) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1092
|
+
const buildRepositoryGuid = await resolveAdoBuildRepositoryGuid(project, token, orgBase, 'ADO live build check', { timeout: 4000 });
|
|
1093
|
+
if (!buildRepositoryGuid) {
|
|
1094
|
+
buildStatusStale = true;
|
|
1095
|
+
buildStatusDetail = 'ADO Builds API requires a repository GUID; repository GUID could not be resolved from project.repositoryId/project.repoName';
|
|
1096
|
+
} else {
|
|
1097
|
+
try {
|
|
1098
|
+
const mergeRef = encodeURIComponent(`refs/pull/${prNum}/merge`);
|
|
1099
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodeURIComponent(buildRepositoryGuid)}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
1100
|
+
const buildsData = await adoFetch(buildsUrl, token, { timeout: 4000 });
|
|
1101
|
+
const allBuilds = buildsData?.value || [];
|
|
1102
|
+
const prBuilds = allBuilds.filter(b => b.sourceVersion === mergeCommitId);
|
|
1103
|
+
if (prBuilds.length > 0) {
|
|
1104
|
+
buildStatus = classifyBuildStatus(prBuilds);
|
|
1105
|
+
} else if (allBuilds.length === 0) {
|
|
1106
|
+
buildStatus = 'none';
|
|
1107
|
+
}
|
|
1108
|
+
// else: merge-commit mismatch — leave buildStatus null so caller
|
|
1109
|
+
// falls back to cached state (issue #1233).
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
buildStatusStale = true;
|
|
1112
|
+
buildStatusDetail = `ADO live build query failed: ${e.message}`;
|
|
1113
|
+
log('warn', `Live build check builds query for ${pr.id}: ${e.message}`);
|
|
975
1114
|
}
|
|
976
|
-
|
|
977
|
-
// falls back to cached state (issue #1233).
|
|
978
|
-
} catch (e) { log('warn', `Live build check builds query for ${pr.id}: ${e.message}`); }
|
|
1115
|
+
}
|
|
979
1116
|
} else {
|
|
980
1117
|
// No merge commit yet — likely conflict or fresh PR. Treat as 'none'
|
|
981
1118
|
// so a stale 'failing' cache can be cleared by the caller.
|
|
@@ -983,7 +1120,11 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
983
1120
|
}
|
|
984
1121
|
}
|
|
985
1122
|
|
|
986
|
-
return {
|
|
1123
|
+
return {
|
|
1124
|
+
buildStatus,
|
|
1125
|
+
mergeConflict,
|
|
1126
|
+
...(buildStatusStale ? { buildStatusStale, buildStatusDetail } : {}),
|
|
1127
|
+
};
|
|
987
1128
|
} catch (e) {
|
|
988
1129
|
log('warn', `Live build/conflict check for ${pr.id}: ${e.message}`);
|
|
989
1130
|
return null;
|
|
@@ -1007,7 +1148,8 @@ async function fetchAdoPrMetadata(prNum, adoOrg, adoProj, adoRepo) {
|
|
|
1007
1148
|
/**
|
|
1008
1149
|
* Fetch live PR and build status for a single PR number.
|
|
1009
1150
|
* Used by engine/ado-status.js so agents can check CI without raw curl calls.
|
|
1010
|
-
* Returns { prNumber, title, branch, status, reviewStatus, buildStatus,
|
|
1151
|
+
* Returns { prNumber, title, branch, status, reviewStatus, buildStatus,
|
|
1152
|
+
* buildStatusStale?, buildStatusDetail?, buildErrorLog?,
|
|
1011
1153
|
* mergeConflict, url, project } or null on auth failure.
|
|
1012
1154
|
*/
|
|
1013
1155
|
async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
@@ -1015,29 +1157,45 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
|
1015
1157
|
if (!token) return null;
|
|
1016
1158
|
|
|
1017
1159
|
const orgBase = getAdoOrgBase(project);
|
|
1018
|
-
const
|
|
1019
|
-
if (!
|
|
1160
|
+
const adoRepositoryLookupKey = getAdoRepositoryLookupKey(project);
|
|
1161
|
+
if (!adoRepositoryLookupKey) {
|
|
1020
1162
|
logMissingAdoRepository(project, 'ADO single PR status fetch');
|
|
1021
1163
|
return null;
|
|
1022
1164
|
}
|
|
1023
|
-
const encodedRepoId = encodeURIComponent(
|
|
1165
|
+
const encodedRepoId = encodeURIComponent(adoRepositoryLookupKey);
|
|
1024
1166
|
const repoBase = `${orgBase}/${project.adoProject}/_apis/git/repositories/${encodedRepoId}`;
|
|
1025
|
-
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
1026
|
-
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodedRepoId}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
1027
1167
|
|
|
1028
|
-
// Fetch PR metadata and
|
|
1029
|
-
const [prData,
|
|
1168
|
+
// Fetch PR metadata and resolve the Builds API repository GUID in parallel.
|
|
1169
|
+
const [prData, buildRepositoryGuid] = await Promise.all([
|
|
1030
1170
|
adoFetch(`${repoBase}/pullrequests/${prNumber}?api-version=7.1`, token),
|
|
1031
|
-
|
|
1171
|
+
resolveAdoBuildRepositoryGuid(project, token, orgBase, 'ADO single PR status fetch'),
|
|
1032
1172
|
]);
|
|
1033
1173
|
if (!prData) return null;
|
|
1034
1174
|
|
|
1175
|
+
let buildsData = null;
|
|
1176
|
+
let buildStatusStale = false;
|
|
1177
|
+
let buildStatusDetail = '';
|
|
1178
|
+
if (buildRepositoryGuid) {
|
|
1179
|
+
try {
|
|
1180
|
+
const mergeRef = encodeURIComponent(`refs/pull/${prNumber}/merge`);
|
|
1181
|
+
const buildsUrl = `${orgBase}/${project.adoProject}/_apis/build/builds?branchName=${mergeRef}&repositoryId=${encodeURIComponent(buildRepositoryGuid)}&repositoryType=TfsGit&$top=25&api-version=7.1`;
|
|
1182
|
+
buildsData = await adoFetch(buildsUrl, token);
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
buildStatusStale = true;
|
|
1185
|
+
buildStatusDetail = `ADO build query failed: ${e.message}`;
|
|
1186
|
+
log('warn', `fetchSinglePrBuildStatus builds query for PR #${prNumber}: ${e.message}`);
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
buildStatusStale = true;
|
|
1190
|
+
buildStatusDetail = 'ADO Builds API requires a repository GUID; repository GUID could not be resolved from project.repositoryId/project.repoName';
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1035
1193
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
1036
1194
|
const prBuilds = mergeCommitId
|
|
1037
1195
|
? (buildsData?.value || []).filter(b => b.sourceVersion === mergeCommitId)
|
|
1038
1196
|
: [];
|
|
1039
1197
|
|
|
1040
|
-
let buildStatus = classifyBuildStatus(prBuilds);
|
|
1198
|
+
let buildStatus = buildStatusStale ? null : classifyBuildStatus(prBuilds);
|
|
1041
1199
|
let buildErrorLog = null;
|
|
1042
1200
|
|
|
1043
1201
|
if (buildStatus === 'failing') {
|
|
@@ -1067,6 +1225,7 @@ async function fetchSinglePrBuildStatus(project, prNumber) {
|
|
|
1067
1225
|
status: prData.status || 'unknown',
|
|
1068
1226
|
reviewStatus: votesToReviewStatus(votes),
|
|
1069
1227
|
buildStatus,
|
|
1228
|
+
...(buildStatusStale ? { buildStatusStale, buildStatusDetail } : {}),
|
|
1070
1229
|
...(buildErrorLog ? { buildErrorLog } : {}),
|
|
1071
1230
|
mergeConflict: prData.mergeStatus === 'conflicts',
|
|
1072
1231
|
url: prUrl,
|
package/engine/lifecycle.js
CHANGED
|
@@ -787,9 +787,15 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
787
787
|
if (!output || !String(output).trim()) {
|
|
788
788
|
const today = dateStamp();
|
|
789
789
|
const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
|
|
790
|
+
const currentItemId = meta?.item?.id ? String(meta.item.id) : '';
|
|
791
|
+
function isCurrentItemInboxNote(fileName, content) {
|
|
792
|
+
if (!currentItemId) return true;
|
|
793
|
+
return String(fileName || '').includes(currentItemId) || String(content || '').includes(currentItemId);
|
|
794
|
+
}
|
|
790
795
|
for (const f of inboxFiles) {
|
|
791
796
|
const content = safeRead(path.join(INBOX_DIR, f));
|
|
792
797
|
if (!content) continue;
|
|
798
|
+
if (!isCurrentItemInboxNote(f, content)) continue;
|
|
793
799
|
const prHeaderPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request|E2E\s+PR)\s+(?:created|opened|submitted)\*{0,2}\s*[:\-]\s*([^\n]+)/gi;
|
|
794
800
|
while ((match = prHeaderPattern.exec(content)) !== null) {
|
|
795
801
|
addPrUrlEvidence(match[1]);
|
|
@@ -1698,6 +1704,119 @@ function normalizeCompletionStatus(status) {
|
|
|
1698
1704
|
return String(status || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
1699
1705
|
}
|
|
1700
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
|
+
|
|
1701
1820
|
function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, exitCode) {
|
|
1702
1821
|
if (!dispatchItem?.id || !outcome) {
|
|
1703
1822
|
log('warn', 'Cannot write non-clean agent report without dispatch id and outcome');
|
|
@@ -1834,6 +1953,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1834
1953
|
// (P-2a6d9c4f, P-9c4f2d6a) populate dispatchItem.meta.runtimeName at spawn time.
|
|
1835
1954
|
const runtimeName = dispatchItem.meta?.runtimeName || dispatchItem.runtimeName || 'claude';
|
|
1836
1955
|
const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout, runtimeName);
|
|
1956
|
+
const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
|
|
1837
1957
|
|
|
1838
1958
|
// Try structured completion protocol first (```completion block from agent output)
|
|
1839
1959
|
const structuredCompletion = parseStructuredCompletion(stdout, runtimeName);
|
|
@@ -1872,9 +1992,11 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1872
1992
|
const effectiveSuccess = isSuccess || autoRecovered;
|
|
1873
1993
|
|
|
1874
1994
|
const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
|
|
1995
|
+
let nonCleanReportWritten = false;
|
|
1875
1996
|
if (completionStatus.startsWith('partial') || autoRecovered || (completionStatus.startsWith('fail') && isSuccess)) {
|
|
1876
1997
|
const outcome = completionStatus.startsWith('fail') ? 'failure' : 'partial';
|
|
1877
|
-
writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion,
|
|
1998
|
+
writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, completionGateSummary, code);
|
|
1999
|
+
nonCleanReportWritten = true;
|
|
1878
2000
|
}
|
|
1879
2001
|
|
|
1880
2002
|
// Handle decomposition results — create sub-items from decompose agent output
|
|
@@ -1981,6 +2103,18 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1981
2103
|
}
|
|
1982
2104
|
|
|
1983
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
|
+
|
|
1984
2118
|
if (effectiveSuccess && meta?.item?.id && !skipDoneStatus) {
|
|
1985
2119
|
completionContractFailure = await enforcePrAttachmentContract(type, meta, agentId, config, resultSummary);
|
|
1986
2120
|
if (completionContractFailure) skipDoneStatus = true;
|
|
@@ -2316,6 +2450,7 @@ module.exports = {
|
|
|
2316
2450
|
parseReviewVerdict,
|
|
2317
2451
|
isReviewBailout,
|
|
2318
2452
|
parseStructuredCompletion,
|
|
2453
|
+
detectNonTerminalResultSummary,
|
|
2319
2454
|
runPostCompletionHooks,
|
|
2320
2455
|
syncPrdFromPrs,
|
|
2321
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/engine.js
CHANGED
|
@@ -2383,6 +2383,16 @@ async function discoverFromPrs(config, project) {
|
|
|
2383
2383
|
try {
|
|
2384
2384
|
const checkBcFn = project.repoHost === 'github' ? ghCheckLiveBuildAndConflict : adoCheckLiveBuildAndConflict;
|
|
2385
2385
|
const live = await checkBcFn(pr, project);
|
|
2386
|
+
if (live?.buildStatusStale) {
|
|
2387
|
+
try {
|
|
2388
|
+
mutatePullRequests(projectPrPath(project), prs => {
|
|
2389
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
2390
|
+
if (!target) return;
|
|
2391
|
+
target._buildStatusStale = true;
|
|
2392
|
+
if (live.buildStatusDetail) target._buildStatusDetail = live.buildStatusDetail;
|
|
2393
|
+
});
|
|
2394
|
+
} catch {}
|
|
2395
|
+
}
|
|
2386
2396
|
if (live && live.buildStatus && live.buildStatus !== 'failing') {
|
|
2387
2397
|
log('info', `Pre-dispatch build check: ${pr.id} build is ${live.buildStatus} (cached was failing) — skipping build-fix`);
|
|
2388
2398
|
// Persist the fresh status so subsequent ticks don't re-check on every pass
|
|
@@ -2391,6 +2401,8 @@ async function discoverFromPrs(config, project) {
|
|
|
2391
2401
|
const target = shared.findPrRecord(prs, pr, project);
|
|
2392
2402
|
if (!target) return;
|
|
2393
2403
|
target.buildStatus = live.buildStatus;
|
|
2404
|
+
delete target._buildStatusStale;
|
|
2405
|
+
delete target._buildStatusDetail;
|
|
2394
2406
|
if (live.buildStatus === 'passing') {
|
|
2395
2407
|
delete target.buildErrorLog;
|
|
2396
2408
|
delete target.buildFailReason;
|
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"
|