@yemi33/minions 0.1.1723 → 0.1.1725
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dashboard/js/command-center.js +25 -6
- package/dashboard/js/command-history.js +1 -1
- package/dashboard/js/detail-panel.js +3 -3
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-dispatch.js +3 -3
- package/dashboard/js/render-inbox.js +1 -2
- package/dashboard/js/render-meetings.js +1 -1
- package/dashboard/js/render-pipelines.js +46 -17
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-schedules.js +2 -2
- package/dashboard/js/render-watches.js +3 -3
- package/dashboard/js/render-work-items.js +3 -3
- package/dashboard/js/utils.js +34 -0
- package/dashboard/styles.css +8 -0
- package/dashboard.js +74 -0
- package/engine/ado.js +27 -0
- package/engine/copilot-models.json +1 -1
- package/engine/github.js +28 -0
- package/engine/lifecycle.js +36 -4
- package/engine/shared.js +37 -0
- package/engine.js +104 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -934,7 +934,13 @@ function ccRetryLast(tabId, retryId) {
|
|
|
934
934
|
|
|
935
935
|
async function _ccFetch(url, body) {
|
|
936
936
|
var res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
937
|
-
if (!res.ok) {
|
|
937
|
+
if (!res.ok) {
|
|
938
|
+
var d = await res.json().catch(function() { return {}; });
|
|
939
|
+
var err = new Error(d.error || 'Request failed (' + res.status + ')');
|
|
940
|
+
err.status = res.status;
|
|
941
|
+
err.data = d;
|
|
942
|
+
throw err;
|
|
943
|
+
}
|
|
938
944
|
return res;
|
|
939
945
|
}
|
|
940
946
|
|
|
@@ -946,6 +952,8 @@ function _tagServerExecuted(actions, actionResults) {
|
|
|
946
952
|
if (r && r.ok) {
|
|
947
953
|
actions[i]._serverExecuted = true;
|
|
948
954
|
if (r.id) actions[i]._serverId = r.id;
|
|
955
|
+
if (r.warning) actions[i]._serverWarning = r.warning;
|
|
956
|
+
if (r.duplicate) actions[i]._serverDuplicate = true;
|
|
949
957
|
} else if (r && r.error) {
|
|
950
958
|
actions[i]._serverExecuted = true;
|
|
951
959
|
actions[i]._serverError = r.error;
|
|
@@ -965,8 +973,10 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
965
973
|
status.style.color = 'var(--red)';
|
|
966
974
|
} else {
|
|
967
975
|
var label = action._serverId ? escHtml(action._serverId) : escHtml(action.title || action.type);
|
|
968
|
-
status.innerHTML = '✓ ' + escHtml(action.type) + ': <strong>' + label + '</strong>'
|
|
969
|
-
|
|
976
|
+
status.innerHTML = '✓ ' + escHtml(action.type) + ': <strong>' + label + '</strong>' +
|
|
977
|
+
(action._serverDuplicate ? ' <span style="color:var(--orange)">already exists</span>' : '') +
|
|
978
|
+
(action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '');
|
|
979
|
+
status.style.color = action._serverDuplicate ? 'var(--orange)' : 'var(--green)';
|
|
970
980
|
}
|
|
971
981
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
972
982
|
if (['dispatch','fix','implement','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
|
|
@@ -1285,9 +1295,18 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1285
1295
|
break;
|
|
1286
1296
|
}
|
|
1287
1297
|
case 'create-pipeline': {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1298
|
+
try {
|
|
1299
|
+
await _ccFetch('/api/pipelines', { id: action.id, title: action.title, stages: action.stages || [], trigger: action.trigger || null, stopWhen: action.stopWhen || null, monitoredResources: action.monitoredResources || null });
|
|
1300
|
+
status.innerHTML = '✓ Pipeline created: <strong>' + escHtml(action.id) + '</strong>';
|
|
1301
|
+
status.style.color = 'var(--green)';
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
if (e.status === 409) {
|
|
1304
|
+
status.innerHTML = '✓ Pipeline already exists: <strong>' + escHtml(action.id) + '</strong>';
|
|
1305
|
+
status.style.color = 'var(--orange)';
|
|
1306
|
+
} else {
|
|
1307
|
+
throw e;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1291
1310
|
break;
|
|
1292
1311
|
}
|
|
1293
1312
|
case 'delete-pipeline': {
|
|
@@ -38,7 +38,7 @@ function cmdShowHistory() {
|
|
|
38
38
|
'<div class="cmd-history-item-meta">' +
|
|
39
39
|
'<span class="chip" style="color:' + intentColor + '">' + intentLabel + '</span>' +
|
|
40
40
|
'<span>' + ago + '</span>' +
|
|
41
|
-
'<span>' +
|
|
41
|
+
'<span>' + formatLocalDateTime(date) + '</span>' +
|
|
42
42
|
'</div>' +
|
|
43
43
|
'</div>' +
|
|
44
44
|
'<button class="cmd-history-resubmit" onclick="cmdResubmit(' + i + ')">Resubmit</button>' +
|
|
@@ -42,8 +42,8 @@ function renderDetailContent(detail, tab) {
|
|
|
42
42
|
html += '<h4>Current Status</h4><div class="section">';
|
|
43
43
|
html += 'Status: <span style="color:var(--' + (detail.statusData.status === 'working' ? 'yellow' : detail.statusData.status === 'done' ? 'green' : 'muted') + ')">' + (detail.statusData.status || 'idle').toUpperCase() + '</span>\n';
|
|
44
44
|
if (detail.statusData.task) html += 'Task: ' + escHtml(detail.statusData.task) + '\n';
|
|
45
|
-
if (detail.statusData.started_at) html += 'Started: ' + detail.statusData.started_at + '\n';
|
|
46
|
-
if (detail.statusData.completed_at) html += 'Completed: ' + detail.statusData.completed_at + '\n';
|
|
45
|
+
if (detail.statusData.started_at) html += 'Started: ' + formatLocalDateTime(detail.statusData.started_at) + '\n';
|
|
46
|
+
if (detail.statusData.completed_at) html += 'Completed: ' + formatLocalDateTime(detail.statusData.completed_at) + '\n';
|
|
47
47
|
if (detail.statusData.started_at && detail.statusData.completed_at) {
|
|
48
48
|
var dMs = new Date(detail.statusData.completed_at).getTime() - new Date(detail.statusData.started_at).getTime();
|
|
49
49
|
if (dMs > 0) {
|
|
@@ -132,7 +132,7 @@ function renderDetailContent(detail, tab) {
|
|
|
132
132
|
'<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(d.task) + '">' + escHtml(d.task.slice(0, 80)) + '</td>' +
|
|
133
133
|
'<td><span class="dispatch-type ' + d.type + '">' + escHtml(d.type) + '</span></td>' +
|
|
134
134
|
'<td style="color:' + color + '"' + reason + '>' + escHtml(d.result) + (isError && d.reason ? ' <span style="font-size:10px;color:var(--muted)">(' + escHtml(d.reason.slice(0, 50)) + ')</span>' : '') + '</td>' +
|
|
135
|
-
'<td style="font-size:10px;color:var(--muted)">' + (d.completed_at ?
|
|
135
|
+
'<td style="font-size:10px;color:var(--muted)">' + (d.completed_at ? formatLocalDateTime(d.completed_at) : '') + '</td>' +
|
|
136
136
|
'</tr>';
|
|
137
137
|
});
|
|
138
138
|
html += '</tbody></table>';
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -60,7 +60,7 @@ function _processStatusUpdate(data) {
|
|
|
60
60
|
localStorage.setItem('minions-install-id', data.installId);
|
|
61
61
|
}
|
|
62
62
|
// Always update cheap elements
|
|
63
|
-
document.getElementById('ts').textContent =
|
|
63
|
+
document.getElementById('ts').textContent = formatLocalTime(data.timestamp);
|
|
64
64
|
const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
|
|
65
65
|
document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
|
|
66
66
|
const autoEl = document.getElementById('auto-approve-badge');
|
|
@@ -75,7 +75,7 @@ function renderAdoThrottleAlert(adoThrottle) {
|
|
|
75
75
|
el.innerHTML = '';
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
const resumeTime =
|
|
78
|
+
const resumeTime = formatLocalTime(adoThrottle.retryAfter);
|
|
79
79
|
el.innerHTML =
|
|
80
80
|
'<span class="engine-alert-msg">⚠️ ADO rate-limited — resume ~' + resumeTime + ' (' + adoThrottle.consecutiveHits + ' consecutive hit' + (adoThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
81
81
|
el.style.display = 'flex';
|
|
@@ -89,7 +89,7 @@ function renderGhThrottleAlert(ghThrottle) {
|
|
|
89
89
|
el.innerHTML = '';
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
-
const resumeTime =
|
|
92
|
+
const resumeTime = formatLocalTime(ghThrottle.retryAfter);
|
|
93
93
|
el.innerHTML =
|
|
94
94
|
'<span class="engine-alert-msg">⚠️ GitHub rate-limited — resume ~' + resumeTime + ' (' + ghThrottle.consecutiveHits + ' consecutive hit' + (ghThrottle.consecutiveHits !== 1 ? 's' : '') + ')</span>';
|
|
95
95
|
el.style.display = 'flex';
|
|
@@ -212,7 +212,7 @@ function renderEngineLog(log) {
|
|
|
212
212
|
|
|
213
213
|
function shortTime(t) {
|
|
214
214
|
if (!t) return '';
|
|
215
|
-
|
|
215
|
+
return formatLocalTime(t);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
async function showErrorDetails(agentId, reason, task) {
|
|
@@ -81,8 +81,7 @@ function renderNotes(notes) {
|
|
|
81
81
|
// Show last updated timestamp
|
|
82
82
|
const updatedEl = document.getElementById('notes-updated');
|
|
83
83
|
if (updatedEl && updatedAt) {
|
|
84
|
-
|
|
85
|
-
updatedEl.textContent = 'updated ' + d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
|
84
|
+
updatedEl.textContent = 'updated ' + formatLocalDateTime(updatedAt);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
if (!content || !content.trim()) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
@@ -46,7 +46,7 @@ function renderMeetings(meetings) {
|
|
|
46
46
|
}).join(' ');
|
|
47
47
|
|
|
48
48
|
const dt = m.completedAt || m.createdAt;
|
|
49
|
-
const timeStr = dt ?
|
|
49
|
+
const timeStr = dt ? formatLocalDateTime(dt) : '';
|
|
50
50
|
|
|
51
51
|
return '<div data-file="meetings/' + escHtml(m.id) + '.json" style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer;position:relative" onclick="if(shouldIgnoreSelectionClick(event))return;openMeetingDetail(\'' + escHtml(m.id) + '\')">' +
|
|
52
52
|
'<div style="display:flex;justify-content:space-between;align-items:center">' +
|
|
@@ -61,6 +61,26 @@ function _renderMonitoredResources(resources, options) {
|
|
|
61
61
|
return '<div style="margin-top:4px;display:flex;flex-wrap:wrap;gap:3px;align-items:center">' + heading + pills.join('') + '</div>';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function _getPipelineActiveRun(pipeline) {
|
|
65
|
+
return ((pipeline && pipeline.runs) || []).find(function(r) { return r.status === 'running' || r.status === 'paused'; });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _getPipelineTriggerLabel(pipeline) {
|
|
69
|
+
return pipeline?.trigger?.cron ? _cronToHuman(pipeline.trigger.cron) : 'Manual trigger';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _getPipelineStageLabel(pipeline) {
|
|
73
|
+
var count = ((pipeline && pipeline.stages) || []).length;
|
|
74
|
+
return count + ' stage' + (count === 1 ? '' : 's');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _getPipelineEmptyRunCopy(pipeline) {
|
|
78
|
+
if (pipeline?.trigger?.cron) {
|
|
79
|
+
return 'No runs yet. Scheduled for ' + _cronToHuman(pipeline.trigger.cron) + ' (' + Intl.DateTimeFormat().resolvedOptions().timeZone + '). Use Run Now to start the first run immediately.';
|
|
80
|
+
}
|
|
81
|
+
return 'No runs yet. Use Run Now to start this pipeline.';
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
/**
|
|
65
85
|
* Render clickable artifact links for a pipeline stage.
|
|
66
86
|
* Each artifact type gets an icon and navigates to the relevant detail view.
|
|
@@ -233,13 +253,14 @@ function _buildNodeChain(stages, run, options) {
|
|
|
233
253
|
|
|
234
254
|
html += '</div>';
|
|
235
255
|
|
|
236
|
-
//
|
|
256
|
+
// Repeat indicator — cron pipelines repeat on schedule; condition/stopWhen pipelines repeat until a terminal condition.
|
|
237
257
|
var hasStopWhen = !!pipeline?.stopWhen;
|
|
238
258
|
var hasConditionStage = (pipeline?.stages || []).some(function(s) { return s.type === 'condition'; });
|
|
239
|
-
|
|
259
|
+
var hasCron = !!pipeline?.trigger?.cron;
|
|
260
|
+
if (hasStopWhen || hasConditionStage || hasCron) {
|
|
240
261
|
var runCount = (pipeline.runs || []).length;
|
|
241
262
|
var cronLabel = pipeline?.trigger?.cron ? _cronToHuman(pipeline.trigger.cron) : 'until condition met';
|
|
242
|
-
html += '<div class="pl-node-loop">\u21BA Loop (' + escHtml(cronLabel) + ')';
|
|
263
|
+
html += '<div class="pl-node-loop">\u21BA ' + (hasCron ? 'Repeats' : 'Loop') + ' (' + escHtml(cronLabel) + ')';
|
|
243
264
|
if (runCount > 0) html += ' \u00b7 Run ' + runCount;
|
|
244
265
|
html += '</div>';
|
|
245
266
|
}
|
|
@@ -261,11 +282,11 @@ function renderPipelines(pipelines) {
|
|
|
261
282
|
countEl.textContent = pipelines.length;
|
|
262
283
|
|
|
263
284
|
el.innerHTML = pipelines.map(function(p) {
|
|
264
|
-
const activeRun = (p
|
|
285
|
+
const activeRun = _getPipelineActiveRun(p);
|
|
265
286
|
const lastRun = (p.runs || []).slice(-1)[0];
|
|
266
287
|
const statusColor = activeRun ? 'var(--blue)' : lastRun?.status === 'completed' ? 'var(--green)' : lastRun?.status === 'failed' ? 'var(--red)' : lastRun?.status === 'stopped' ? 'var(--yellow)' : 'var(--muted)';
|
|
267
|
-
const statusLabel = activeRun ? 'Running' : lastRun ? (lastRun.status === 'completed' ? 'Completed' : lastRun.status === 'failed' ? 'Failed' : lastRun.status === 'stopped' ? 'Stopped' : lastRun.status) : '
|
|
268
|
-
const trigger =
|
|
288
|
+
const statusLabel = activeRun ? (activeRun.status === 'paused' ? 'Paused' : 'Running') : lastRun ? (lastRun.status === 'completed' ? 'Completed' : lastRun.status === 'failed' ? 'Failed' : lastRun.status === 'stopped' ? 'Stopped' : lastRun.status) : 'Not run yet';
|
|
289
|
+
const trigger = _getPipelineTriggerLabel(p);
|
|
269
290
|
|
|
270
291
|
// Build node chain (renders for all pipelines, even never-run)
|
|
271
292
|
var progressHtml = '';
|
|
@@ -278,12 +299,18 @@ function renderPipelines(pipelines) {
|
|
|
278
299
|
var allResources = _collectMonitoredResources(p);
|
|
279
300
|
var resourcesHtml = _renderMonitoredResources(allResources, { compact: true });
|
|
280
301
|
|
|
281
|
-
return '<div
|
|
282
|
-
'<div
|
|
283
|
-
'<
|
|
284
|
-
|
|
285
|
-
'<
|
|
286
|
-
|
|
302
|
+
return '<div class="pipeline-card" onclick="if(shouldIgnoreSelectionClick(event))return;openPipelineDetail(\'' + escHtml(p.id) + '\')">' +
|
|
303
|
+
'<div class="pipeline-card-header">' +
|
|
304
|
+
'<div class="pipeline-card-main">' +
|
|
305
|
+
'<strong class="pipeline-card-title">' + escHtml(p.title || p.id) + '</strong>' +
|
|
306
|
+
'<div class="pipeline-card-meta">' +
|
|
307
|
+
'<span>' + escHtml(p.id) + '</span>' +
|
|
308
|
+
'<span>' + escHtml(_getPipelineStageLabel(p)) + '</span>' +
|
|
309
|
+
'<span>' + escHtml(trigger) + '</span>' +
|
|
310
|
+
'</div>' +
|
|
311
|
+
'</div>' +
|
|
312
|
+
'<div class="pipeline-card-badges">' +
|
|
313
|
+
'<span style="color:' + statusColor + ';font-size:11px;font-weight:600">' + escHtml(statusLabel) + '</span>' +
|
|
287
314
|
(p.stopWhen ? '<span style="font-size:9px;color:var(--yellow)" title="Auto-stops when condition met: ' + escHtml(typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || 'condition')) + '">STOP-WHEN</span>' : '') +
|
|
288
315
|
(p.enabled === false ? '<span style="font-size:9px;color:var(--red)"' + (p._stopReason ? ' title="' + escHtml(p._stopReason) + '"' : '') + '>' + (p._stoppedBy ? 'AUTO-STOPPED' : 'DISABLED') + '</span>' : '') +
|
|
289
316
|
'</div>' +
|
|
@@ -302,9 +329,9 @@ function openPipelineDetail(id) {
|
|
|
302
329
|
var html = '<div style="display:flex;flex-direction:column;gap:12px">';
|
|
303
330
|
|
|
304
331
|
// Status + actions
|
|
305
|
-
var activeRun = (p
|
|
332
|
+
var activeRun = _getPipelineActiveRun(p);
|
|
306
333
|
html += '<div style="display:flex;justify-content:space-between;align-items:center">' +
|
|
307
|
-
'<span style="font-size:10px;color:var(--muted)">' + (p.trigger?.cron ? escHtml(_cronToHuman(p.trigger.cron)) + ' <span style="opacity:0.6">(' + escHtml(p.trigger.cron) + ', ' + escHtml(Intl.DateTimeFormat().resolvedOptions().timeZone) + ')</span>' : 'Manual trigger') + '</span>' +
|
|
334
|
+
'<span style="font-size:10px;color:var(--muted)">' + (p.trigger?.cron ? escHtml(_cronToHuman(p.trigger.cron)) + ' <span style="opacity:0.6">(' + escHtml(p.trigger.cron) + ', ' + escHtml(Intl.DateTimeFormat().resolvedOptions().timeZone) + ')</span>' : 'Manual trigger') + ' · ' + escHtml(_getPipelineStageLabel(p)) + '</span>' +
|
|
308
335
|
'<div style="display:flex;gap:6px">' +
|
|
309
336
|
(activeRun
|
|
310
337
|
? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_abortPipeline(\'' + escHtml(id) + '\',this)">Abort</button>' +
|
|
@@ -335,7 +362,7 @@ function openPipelineDetail(id) {
|
|
|
335
362
|
var swLabel = typeof p.stopWhen === 'string' ? p.stopWhen : (p.stopWhen.check || JSON.stringify(p.stopWhen));
|
|
336
363
|
html += '<div style="border:1px solid color-mix(in srgb, var(--yellow) 30%, transparent);border-radius:6px;padding:4px 10px;background:color-mix(in srgb, var(--yellow) 6%, transparent);font-size:11px">' +
|
|
337
364
|
'<span style="color:var(--yellow);font-weight:600">Stop When:</span> <span style="color:var(--text)">' + escHtml(swLabel) + '</span>' +
|
|
338
|
-
|
|
365
|
+
(p._stoppedBy ? ' <span style="color:var(--green);font-size:10px">\u2714 triggered' + (p._stoppedAt ? ' at ' + escHtml(formatLocalDateTime(p._stoppedAt)) : '') + '</span>' : '') +
|
|
339
366
|
'</div>';
|
|
340
367
|
}
|
|
341
368
|
if (p._stopReason && p.enabled === false) {
|
|
@@ -379,13 +406,15 @@ function openPipelineDetail(id) {
|
|
|
379
406
|
html += '<div style="font-size:10px">' +
|
|
380
407
|
'<div style="display:flex;gap:8px;align-items:center">' +
|
|
381
408
|
'<span style="color:' + color + ';font-weight:600">' + r.status + '</span>' +
|
|
382
|
-
'<span style="color:var(--muted)">' + (r.startedAt ?
|
|
383
|
-
(r.completedAt ? '<span style="color:var(--muted)">\u2192 ' +
|
|
409
|
+
'<span style="color:var(--muted)">' + (r.startedAt ? formatLocalDateTime(r.startedAt) : '') + '</span>' +
|
|
410
|
+
(r.completedAt ? '<span style="color:var(--muted)">\u2192 ' + formatLocalDateTime(r.completedAt) + '</span>' : '') +
|
|
384
411
|
(artifactCount > 0 ? '<span style="color:var(--blue);cursor:pointer;user-select:none" onclick="var el=document.getElementById(\'' + toggleId + '\');el.style.display=el.style.display===\'none\'?\'flex\':\'none\'" title="Toggle artifacts">' + artifactCount + ' artifact' + (artifactCount !== 1 ? 's' : '') + ' ▾</span>' : '') +
|
|
385
412
|
'</div>' +
|
|
386
413
|
(artifactCount > 0 ? '<div id="' + toggleId + '" style="display:none;flex-wrap:wrap;gap:4px;margin-top:4px;margin-left:12px">' + _renderArtifactLinks(runArtifacts.merged, id) + '</div>' : '') +
|
|
387
414
|
'</div>';
|
|
388
415
|
});
|
|
416
|
+
} else {
|
|
417
|
+
html += '<div class="pipeline-empty-runs">' + escHtml(_getPipelineEmptyRunCopy(p)) + '</div>';
|
|
389
418
|
}
|
|
390
419
|
|
|
391
420
|
html += '</div>';
|
|
@@ -498,7 +498,7 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
|
|
|
498
498
|
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planDelete(\'' + escapeHtml(normalizedFile) + '\')">Delete</button>';
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' +
|
|
501
|
+
const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + formatLocalDateTime(lastMod) + '</div>' : '';
|
|
502
502
|
const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' + modalActions + '</div>';
|
|
503
503
|
|
|
504
504
|
document.getElementById('modal-title').innerHTML = escapeHtml(title) + (versionLabel ? ' <span style="font-size:11px;font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escapeHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
|
|
@@ -589,7 +589,7 @@ function openArchivedPrdModal() {
|
|
|
589
589
|
html += groups.map((g, i) => {
|
|
590
590
|
const done = g.items.filter(it => it.status === 'done').length;
|
|
591
591
|
const failed = g.items.filter(it => it.status === 'failed').length;
|
|
592
|
-
const completed = g.completedAt ?
|
|
592
|
+
const completed = g.completedAt ? formatLocalDate(g.completedAt) : '';
|
|
593
593
|
return '<div class="plan-card" style="cursor:pointer;margin-bottom:8px" onclick="if(shouldIgnoreSelectionClick(event))return;showArchivedPrdDetail(\'' + escHtml(g.file) + '\')">' +
|
|
594
594
|
'<div class="plan-card-title" style="font-size:13px">' + escHtml(g.summary || g.file) + '</div>' +
|
|
595
595
|
'<div class="plan-card-meta">' +
|
|
@@ -18,7 +18,7 @@ function _cronToHuman(cron) {
|
|
|
18
18
|
const m = parseInt(minute, 10);
|
|
19
19
|
if (isNaN(h) || isNaN(m)) return cron;
|
|
20
20
|
|
|
21
|
-
const timeStr =
|
|
21
|
+
const timeStr = formatLocalClock(h, m);
|
|
22
22
|
|
|
23
23
|
if (dow === '*') return 'Daily at ' + timeStr;
|
|
24
24
|
|
|
@@ -332,7 +332,7 @@ function openScheduleDetail(id) {
|
|
|
332
332
|
const s = (window._lastSchedules || []).find(x => x.id === id);
|
|
333
333
|
if (!s) return;
|
|
334
334
|
const humanCron = _cronToHuman(s.cron || '');
|
|
335
|
-
const lastRun = s._lastRun ?
|
|
335
|
+
const lastRun = s._lastRun ? formatLocalDateTime(s._lastRun) : 'never';
|
|
336
336
|
const enabledLabel = s.enabled ? '<span class="pr-badge approved">enabled</span>' : '<span class="pr-badge rejected">disabled</span>';
|
|
337
337
|
|
|
338
338
|
document.getElementById('modal-title').innerHTML = escHtml(s.title || s.id) +
|
|
@@ -137,9 +137,9 @@ function openWatchDetail(id) {
|
|
|
137
137
|
var w = (window._lastWatches || []).find(function(x) { return x.id === id; });
|
|
138
138
|
if (!w) return;
|
|
139
139
|
var statusBadge = _WATCH_STATUS_BADGES[w.status] || escHtml(w.status || '');
|
|
140
|
-
var lastChecked = w.last_checked ?
|
|
141
|
-
var lastTriggered = w.last_triggered ?
|
|
142
|
-
var createdAt = w.created_at ?
|
|
140
|
+
var lastChecked = w.last_checked ? formatLocalDateTime(w.last_checked) : 'never';
|
|
141
|
+
var lastTriggered = w.last_triggered ? formatLocalDateTime(w.last_triggered) : 'never';
|
|
142
|
+
var createdAt = w.created_at ? formatLocalDateTime(w.created_at) : 'unknown';
|
|
143
143
|
var targetLabel = _WATCH_TARGET_LABELS[w.targetType] || w.targetType;
|
|
144
144
|
var condLabel = _WATCH_CONDITION_LABELS[w.condition] || w.condition;
|
|
145
145
|
|
|
@@ -458,9 +458,9 @@ function openWorkItemDetail(id) {
|
|
|
458
458
|
html += field('Description', '<div style="font-size:12px;max-height:320px;overflow-y:auto;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)">' + renderMd(item.description || item.title || '—') + '</div>');
|
|
459
459
|
html += field('Agent', escapeHtml(item.dispatched_to || item.agent || 'Auto'));
|
|
460
460
|
html += field('Source', escapeHtml(item._source || 'central'));
|
|
461
|
-
if (item.created) html += field('Created', escapeHtml(
|
|
462
|
-
if (item.dispatched_at) html += field('Dispatched', escapeHtml(
|
|
463
|
-
if (item.completedAt) html += field('Completed', escapeHtml(
|
|
461
|
+
if (item.created) html += field('Created', escapeHtml(formatLocalDateTime(item.created)));
|
|
462
|
+
if (item.dispatched_at) html += field('Dispatched', escapeHtml(formatLocalDateTime(item.dispatched_at)) + ' to ' + escapeHtml(item.dispatched_to || '?'));
|
|
463
|
+
if (item.completedAt) html += field('Completed', escapeHtml(formatLocalDateTime(item.completedAt)));
|
|
464
464
|
if (item.failReason) html += field('Failure Reason', '<span style="color:var(--red)">' + escapeHtml(item.failReason) + '</span>');
|
|
465
465
|
if (item._pendingReason && item.status === 'pending') html += field('Pending Reason', item._pendingReason === 'already_dispatched' ? 'Queued — waiting for available agent slot' : escapeHtml(item._pendingReason.replace(/_/g, ' ')));
|
|
466
466
|
if (item._skipReason && item.status === 'pending') html += field('Dispatch Blocked', '<span style="color:var(--yellow)">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + '</span>' + (item._blockedBy ? ' — blocked by <strong>' + escapeHtml(item._blockedBy) + '</strong>' : ''));
|
package/dashboard/js/utils.js
CHANGED
|
@@ -151,6 +151,40 @@ function timeAgo(isoStr) {
|
|
|
151
151
|
return d + 'd ago';
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
const _LOCAL_DATE_TIME_OPTS = { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' };
|
|
155
|
+
const _LOCAL_DATE_OPTS = { month: 'short', day: 'numeric', year: 'numeric' };
|
|
156
|
+
const _LOCAL_TIME_OPTS = { hour: 'numeric', minute: '2-digit' };
|
|
157
|
+
|
|
158
|
+
function _coerceDashboardDate(value) {
|
|
159
|
+
if (!value) return null;
|
|
160
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
161
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatLocalDateTime(value) {
|
|
165
|
+
const date = _coerceDashboardDate(value);
|
|
166
|
+
if (!date) return value ? String(value) : '';
|
|
167
|
+
return date.toLocaleString(undefined, _LOCAL_DATE_TIME_OPTS);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatLocalDate(value) {
|
|
171
|
+
const date = _coerceDashboardDate(value);
|
|
172
|
+
if (!date) return value ? String(value) : '';
|
|
173
|
+
return date.toLocaleDateString(undefined, _LOCAL_DATE_OPTS);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatLocalTime(value) {
|
|
177
|
+
const date = _coerceDashboardDate(value);
|
|
178
|
+
if (!date) return value ? String(value) : '';
|
|
179
|
+
return date.toLocaleTimeString(undefined, _LOCAL_TIME_OPTS);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatLocalClock(hour, minute) {
|
|
183
|
+
const date = new Date();
|
|
184
|
+
date.setHours(Number(hour) || 0, Number(minute) || 0, 0, 0);
|
|
185
|
+
return formatLocalTime(date);
|
|
186
|
+
}
|
|
187
|
+
|
|
154
188
|
function statusColor(s) {
|
|
155
189
|
return s === 'working' ? 'working' : s === 'done' ? 'done' : s === 'error' ? 'error' : '';
|
|
156
190
|
}
|
package/dashboard/styles.css
CHANGED
|
@@ -207,6 +207,14 @@
|
|
|
207
207
|
.pl-node-arrow { display: flex; align-items: center; padding: 0 2px; color: var(--muted); font-size: 12px; flex-shrink: 0; margin-top: 8px; }
|
|
208
208
|
.pl-node-meta { font-size: 9px; color: var(--muted); margin-top: 2px; text-align: center; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
|
|
209
209
|
.pl-node-loop { font-size: 10px; color: var(--muted); margin-top: 6px; display: flex; align-items: center; gap: 6px; }
|
|
210
|
+
.pipeline-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 12px 16px; margin-bottom: var(--space-4); cursor: pointer; transition: border-color var(--transition-fast), transform var(--transition-fast); }
|
|
211
|
+
.pipeline-card:hover { border-color: var(--blue); transform: translateY(-1px); }
|
|
212
|
+
.pipeline-card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-6); }
|
|
213
|
+
.pipeline-card-main { min-width: 0; flex: 1; }
|
|
214
|
+
.pipeline-card-title { display: block; font-size: var(--text-lg); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
215
|
+
.pipeline-card-meta { margin-top: var(--space-2); display: flex; flex-wrap: wrap; gap: var(--space-4); font-size: var(--text-sm); color: var(--muted); }
|
|
216
|
+
.pipeline-card-badges { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: var(--space-4); flex-shrink: 0; }
|
|
217
|
+
.pipeline-empty-runs { border: 1px dashed var(--border); border-radius: var(--radius-md); padding: var(--space-6); color: var(--muted); font-size: var(--text-md); background: rgba(139,148,158,0.04); }
|
|
210
218
|
.prd-items-list { display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; padding: 0 8px; }
|
|
211
219
|
.prd-item-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: var(--radius-sm); font-size: var(--text-base); background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--border); }
|
|
212
220
|
.prd-item-row.st-done { border-left-color: var(--green); }
|
package/dashboard.js
CHANGED
|
@@ -1581,6 +1581,70 @@ function meetingParticipantsFromAction(action) {
|
|
|
1581
1581
|
);
|
|
1582
1582
|
}
|
|
1583
1583
|
|
|
1584
|
+
function normalizePipelineForCompare(pipeline) {
|
|
1585
|
+
if (!pipeline || typeof pipeline !== 'object') return null;
|
|
1586
|
+
return {
|
|
1587
|
+
title: pipeline.title || '',
|
|
1588
|
+
stages: Array.isArray(pipeline.stages) ? pipeline.stages : [],
|
|
1589
|
+
trigger: pipeline.trigger && typeof pipeline.trigger === 'object' ? pipeline.trigger : {},
|
|
1590
|
+
enabled: pipeline.enabled !== false,
|
|
1591
|
+
stopWhen: pipeline.stopWhen || null,
|
|
1592
|
+
monitoredResources: Array.isArray(pipeline.monitoredResources) ? pipeline.monitoredResources : [],
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function buildPipelineFromAction(action) {
|
|
1597
|
+
const pipeline = {
|
|
1598
|
+
id: String(action.id || '').trim(),
|
|
1599
|
+
title: String(action.title || '').trim(),
|
|
1600
|
+
stages: action.stages,
|
|
1601
|
+
trigger: action.trigger && typeof action.trigger === 'object' ? action.trigger : {},
|
|
1602
|
+
enabled: action.enabled !== false,
|
|
1603
|
+
};
|
|
1604
|
+
if (action.stopWhen) pipeline.stopWhen = action.stopWhen;
|
|
1605
|
+
if (Array.isArray(action.monitoredResources) && action.monitoredResources.length > 0) {
|
|
1606
|
+
pipeline.monitoredResources = action.monitoredResources;
|
|
1607
|
+
}
|
|
1608
|
+
return pipeline;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function pipelineDefinitionsEqual(a, b) {
|
|
1612
|
+
return JSON.stringify(normalizePipelineForCompare(a)) === JSON.stringify(normalizePipelineForCompare(b));
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function createPipelineFromAction(action) {
|
|
1616
|
+
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
1617
|
+
const pipeline = buildPipelineFromAction(action);
|
|
1618
|
+
const existing = getPipeline(pipeline.id);
|
|
1619
|
+
if (existing) {
|
|
1620
|
+
if (pipelineDefinitionsEqual(existing, pipeline)) {
|
|
1621
|
+
return {
|
|
1622
|
+
type: 'create-pipeline',
|
|
1623
|
+
id: pipeline.id,
|
|
1624
|
+
ok: true,
|
|
1625
|
+
duplicate: true,
|
|
1626
|
+
duplicateOf: pipeline.id,
|
|
1627
|
+
warning: `Pipeline "${pipeline.id}" already exists; no changes made.`,
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
type: 'create-pipeline',
|
|
1632
|
+
id: pipeline.id,
|
|
1633
|
+
error: `Pipeline "${pipeline.id}" already exists with a different definition. Use edit-pipeline to update it.`,
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
savePipeline(pipeline);
|
|
1637
|
+
const persisted = getPipeline(pipeline.id);
|
|
1638
|
+
if (!persisted) {
|
|
1639
|
+
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" was not persisted.` };
|
|
1640
|
+
}
|
|
1641
|
+
if (!pipelineDefinitionsEqual(persisted, pipeline)) {
|
|
1642
|
+
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" persisted with unexpected contents.` };
|
|
1643
|
+
}
|
|
1644
|
+
invalidateStatusCache();
|
|
1645
|
+
return { type: 'create-pipeline', id: pipeline.id, ok: true, created: true };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1584
1648
|
// Required-field validator for CC actions. Returns null when valid, an error string when not.
|
|
1585
1649
|
// Centralises field-required checks so the model can't quietly emit a malformed action and have
|
|
1586
1650
|
// the server silently fall back to placeholder values (e.g. "Untitled"). The handler invokes this
|
|
@@ -1615,6 +1679,11 @@ function _ccValidateAction(action) {
|
|
|
1615
1679
|
if (meetingParticipantsFromAction(action).length < 2) return 'create-meeting action requires at least 2 participants';
|
|
1616
1680
|
return null;
|
|
1617
1681
|
}
|
|
1682
|
+
case 'create-pipeline':
|
|
1683
|
+
if (!action.id || typeof action.id !== 'string' || !action.id.trim()) return 'create-pipeline action missing required field: id';
|
|
1684
|
+
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return 'create-pipeline action missing required field: title';
|
|
1685
|
+
if (!Array.isArray(action.stages) || action.stages.length === 0) return 'create-pipeline action requires non-empty stages array';
|
|
1686
|
+
return null;
|
|
1618
1687
|
default:
|
|
1619
1688
|
return null; // unknown types fall through to existing handler / generic fallback
|
|
1620
1689
|
}
|
|
@@ -1853,6 +1922,10 @@ async function executeCCActions(actions) {
|
|
|
1853
1922
|
results.push({ type: 'create-meeting', id: meeting.id, ok: true });
|
|
1854
1923
|
break;
|
|
1855
1924
|
}
|
|
1925
|
+
case 'create-pipeline': {
|
|
1926
|
+
results.push(createPipelineFromAction(action));
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1856
1929
|
case 'delete-watch': {
|
|
1857
1930
|
const deleted = watchesMod.deleteWatch(action.id);
|
|
1858
1931
|
if (deleted) invalidateStatusCache();
|
|
@@ -6942,6 +7015,7 @@ module.exports = {
|
|
|
6942
7015
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
6943
7016
|
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
6944
7017
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
7018
|
+
_createPipelineFromAction: createPipelineFromAction,
|
|
6945
7019
|
executeCCActions,
|
|
6946
7020
|
};
|
|
6947
7021
|
|
package/engine/ado.js
CHANGED
|
@@ -708,6 +708,7 @@ async function pollPrStatus(config) {
|
|
|
708
708
|
delete pr.buildStatus;
|
|
709
709
|
delete pr.buildFailReason;
|
|
710
710
|
delete pr.buildErrorLog;
|
|
711
|
+
delete pr.buildFailureSignature;
|
|
711
712
|
delete pr._buildFailNotified;
|
|
712
713
|
delete pr._buildStatusStale;
|
|
713
714
|
delete pr._buildStatusDetail;
|
|
@@ -738,6 +739,11 @@ async function pollPrStatus(config) {
|
|
|
738
739
|
pr._adoSourceCommit = sourceCommit;
|
|
739
740
|
updated = true;
|
|
740
741
|
}
|
|
742
|
+
const targetCommit = prData.lastMergeTargetCommit?.commitId || '';
|
|
743
|
+
if (targetCommit && pr._adoTargetCommit !== targetCommit) {
|
|
744
|
+
pr._adoTargetCommit = targetCommit;
|
|
745
|
+
updated = true;
|
|
746
|
+
}
|
|
741
747
|
|
|
742
748
|
const reviewers = prData.reviewers || [];
|
|
743
749
|
const votes = reviewers.map(r => r.vote).filter(v => v !== undefined);
|
|
@@ -811,6 +817,7 @@ async function pollPrStatus(config) {
|
|
|
811
817
|
const mergeCommitId = prData.lastMergeCommit?.commitId;
|
|
812
818
|
let buildStatus = pr.buildStatus || 'none';
|
|
813
819
|
let buildFailReason = pr.buildFailReason || '';
|
|
820
|
+
let buildFailureSignature = pr.buildFailureSignature || '';
|
|
814
821
|
let buildStatusResolved = true;
|
|
815
822
|
let buildStatusStaleDetail = '';
|
|
816
823
|
|
|
@@ -834,6 +841,11 @@ async function pollPrStatus(config) {
|
|
|
834
841
|
if (buildStatus === 'failing') {
|
|
835
842
|
const failed = prBuilds.find(b => b.result === 'failed');
|
|
836
843
|
buildFailReason = failed?.definition?.name || 'Build failed';
|
|
844
|
+
buildFailureSignature = shared.safeSlugComponent([
|
|
845
|
+
failed?.definition?.name,
|
|
846
|
+
failed?.result,
|
|
847
|
+
failed?.status,
|
|
848
|
+
].filter(Boolean).join('\n') || buildFailReason, 80);
|
|
837
849
|
}
|
|
838
850
|
} else if (allBuilds.length > 0 && pr.buildStatus) {
|
|
839
851
|
// Stale merge-commit fallback — ADO returned builds for this PR's merge ref
|
|
@@ -879,6 +891,8 @@ async function pollPrStatus(config) {
|
|
|
879
891
|
pr.buildStatus = buildStatus;
|
|
880
892
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
881
893
|
else delete pr.buildFailReason;
|
|
894
|
+
if (buildFailureSignature) pr.buildFailureSignature = buildFailureSignature;
|
|
895
|
+
else delete pr.buildFailureSignature;
|
|
882
896
|
// Build transitioned — clear grace period and auto-complete flag
|
|
883
897
|
delete pr._buildFixPushedAt;
|
|
884
898
|
if (buildStatus === 'failing') delete pr._autoCompleted;
|
|
@@ -894,6 +908,7 @@ async function pollPrStatus(config) {
|
|
|
894
908
|
// fix agents to be dispatched blind.
|
|
895
909
|
if (buildStatus === 'passing') {
|
|
896
910
|
delete pr.buildErrorLog;
|
|
911
|
+
delete pr.buildFailureSignature;
|
|
897
912
|
// Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
|
|
898
913
|
if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
|
|
899
914
|
}
|
|
@@ -909,6 +924,16 @@ async function pollPrStatus(config) {
|
|
|
909
924
|
} catch {}
|
|
910
925
|
}
|
|
911
926
|
}
|
|
927
|
+
if (buildStatus === 'failing') {
|
|
928
|
+
if (buildFailReason && pr.buildFailReason !== buildFailReason) {
|
|
929
|
+
pr.buildFailReason = buildFailReason;
|
|
930
|
+
updated = true;
|
|
931
|
+
}
|
|
932
|
+
if (buildFailureSignature && pr.buildFailureSignature !== buildFailureSignature) {
|
|
933
|
+
pr.buildFailureSignature = buildFailureSignature;
|
|
934
|
+
updated = true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
912
937
|
}
|
|
913
938
|
|
|
914
939
|
// Auto-complete: set auto-complete on PR when builds green + review approved
|
|
@@ -1041,6 +1066,8 @@ async function pollPrHumanComments(config) {
|
|
|
1041
1066
|
|
|
1042
1067
|
pr.humanFeedback = {
|
|
1043
1068
|
lastProcessedCommentDate: latestDate,
|
|
1069
|
+
lastProcessedCommentId: String(newHumanComments[newHumanComments.length - 1].commentId),
|
|
1070
|
+
lastProcessedCommentKey: `${newHumanComments[newHumanComments.length - 1].threadId}:${newHumanComments[newHumanComments.length - 1].commentId}`,
|
|
1044
1071
|
pendingFix: true,
|
|
1045
1072
|
feedbackContent
|
|
1046
1073
|
};
|
package/engine/github.js
CHANGED
|
@@ -372,6 +372,10 @@ async function pollPrStatus(config) {
|
|
|
372
372
|
pr.lastPushedAt = ts();
|
|
373
373
|
updated = true;
|
|
374
374
|
}
|
|
375
|
+
if (prData.base?.sha && pr.baseSha !== prData.base.sha) {
|
|
376
|
+
pr.baseSha = prData.base.sha;
|
|
377
|
+
updated = true;
|
|
378
|
+
}
|
|
375
379
|
|
|
376
380
|
if (pr.status !== newStatus) {
|
|
377
381
|
log('info', `PR ${pr.id} status: ${pr.status} → ${newStatus}`);
|
|
@@ -389,6 +393,7 @@ async function pollPrStatus(config) {
|
|
|
389
393
|
delete pr.buildStatus;
|
|
390
394
|
delete pr.buildFailReason;
|
|
391
395
|
delete pr.buildErrorLog;
|
|
396
|
+
delete pr.buildFailureSignature;
|
|
392
397
|
delete pr._buildFailNotified;
|
|
393
398
|
delete pr.buildFixAttempts;
|
|
394
399
|
delete pr.buildFixEscalated;
|
|
@@ -471,6 +476,7 @@ async function pollPrStatus(config) {
|
|
|
471
476
|
const runs = checksData.check_runs;
|
|
472
477
|
let buildStatus = 'none';
|
|
473
478
|
let buildFailReason = '';
|
|
479
|
+
let buildFailureSignature = '';
|
|
474
480
|
|
|
475
481
|
if (runs.length > 0) {
|
|
476
482
|
const hasFailed = runs.some(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
@@ -481,6 +487,13 @@ async function pollPrStatus(config) {
|
|
|
481
487
|
buildStatus = 'failing';
|
|
482
488
|
const failed = runs.find(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
483
489
|
buildFailReason = failed?.name || 'Check failed';
|
|
490
|
+
buildFailureSignature = shared.safeSlugComponent([
|
|
491
|
+
failed?.name,
|
|
492
|
+
failed?.conclusion,
|
|
493
|
+
failed?.output?.title,
|
|
494
|
+
failed?.output?.summary,
|
|
495
|
+
failed?.output?.text,
|
|
496
|
+
].filter(Boolean).join('\n') || buildFailReason, 80);
|
|
484
497
|
} else if (allDone && allPassed) {
|
|
485
498
|
buildStatus = 'passing';
|
|
486
499
|
} else {
|
|
@@ -493,6 +506,8 @@ async function pollPrStatus(config) {
|
|
|
493
506
|
pr.buildStatus = buildStatus;
|
|
494
507
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
495
508
|
else delete pr.buildFailReason;
|
|
509
|
+
if (buildFailureSignature) pr.buildFailureSignature = buildFailureSignature;
|
|
510
|
+
else delete pr.buildFailureSignature;
|
|
496
511
|
// Build transitioned — clear grace period and auto-complete flag
|
|
497
512
|
delete pr._buildFixPushedAt;
|
|
498
513
|
if (buildStatus === 'failing') delete pr._autoCompleted; // allow re-merge after fix
|
|
@@ -504,6 +519,7 @@ async function pollPrStatus(config) {
|
|
|
504
519
|
// while a queued build was still running.
|
|
505
520
|
if (buildStatus === 'passing') {
|
|
506
521
|
delete pr.buildErrorLog;
|
|
522
|
+
delete pr.buildFailureSignature;
|
|
507
523
|
// Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
|
|
508
524
|
if (pr.buildFixAttempts) { delete pr.buildFixAttempts; delete pr.buildFixEscalated; }
|
|
509
525
|
}
|
|
@@ -519,6 +535,16 @@ async function pollPrStatus(config) {
|
|
|
519
535
|
} catch {}
|
|
520
536
|
}
|
|
521
537
|
}
|
|
538
|
+
if (buildStatus === 'failing') {
|
|
539
|
+
if (buildFailReason && pr.buildFailReason !== buildFailReason) {
|
|
540
|
+
pr.buildFailReason = buildFailReason;
|
|
541
|
+
updated = true;
|
|
542
|
+
}
|
|
543
|
+
if (buildFailureSignature && pr.buildFailureSignature !== buildFailureSignature) {
|
|
544
|
+
pr.buildFailureSignature = buildFailureSignature;
|
|
545
|
+
updated = true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
522
548
|
}
|
|
523
549
|
}
|
|
524
550
|
|
|
@@ -636,6 +662,8 @@ async function pollPrHumanComments(config) {
|
|
|
636
662
|
|
|
637
663
|
pr.humanFeedback = {
|
|
638
664
|
lastProcessedCommentDate: latestDate,
|
|
665
|
+
lastProcessedCommentId: String(newComments[newComments.length - 1].commentId),
|
|
666
|
+
lastProcessedCommentKey: String(newComments[newComments.length - 1].commentId),
|
|
639
667
|
pendingFix: true,
|
|
640
668
|
feedbackContent
|
|
641
669
|
};
|
package/engine/lifecycle.js
CHANGED
|
@@ -1431,7 +1431,26 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1431
1431
|
}
|
|
1432
1432
|
}
|
|
1433
1433
|
|
|
1434
|
-
function
|
|
1434
|
+
function getHumanFeedbackAutomationCauseKey(pr) {
|
|
1435
|
+
const feedback = pr?.humanFeedback;
|
|
1436
|
+
if (!feedback || typeof feedback !== 'object') return '';
|
|
1437
|
+
const commentRef = feedback.lastProcessedCommentKey
|
|
1438
|
+
|| feedback.lastProcessedCommentId
|
|
1439
|
+
|| feedback.commentId
|
|
1440
|
+
|| feedback.lastProcessedCommentDate
|
|
1441
|
+
|| feedback.feedbackContent
|
|
1442
|
+
|| '';
|
|
1443
|
+
return commentRef ? `human-comment:${shared.safeSlugComponent(commentRef, 80)}` : '';
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function shouldClearHumanFeedbackPendingFix(target, completedPr, automationCauseKey) {
|
|
1447
|
+
if (!target?.humanFeedback?.pendingFix) return true;
|
|
1448
|
+
const currentCauseKey = getHumanFeedbackAutomationCauseKey(target);
|
|
1449
|
+
const completedCauseKey = automationCauseKey || getHumanFeedbackAutomationCauseKey(completedPr);
|
|
1450
|
+
return !currentCauseKey || !completedCauseKey || currentCauseKey === completedCauseKey;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function updatePrAfterFix(pr, project, source, automationCauseKey = '', dispatchId = '') {
|
|
1435
1454
|
|
|
1436
1455
|
if (!pr?.id) return;
|
|
1437
1456
|
const prPath = project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json');
|
|
@@ -1442,13 +1461,26 @@ function updatePrAfterFix(pr, project, source) {
|
|
|
1442
1461
|
// Never downgrade from approved — fix was dispatched but PR is already approved
|
|
1443
1462
|
if (target.reviewStatus !== 'approved') target.reviewStatus = 'waiting';
|
|
1444
1463
|
if (source === 'pr-human-feedback') {
|
|
1445
|
-
|
|
1464
|
+
const clearPendingFix = shouldClearHumanFeedbackPendingFix(target, pr, automationCauseKey);
|
|
1465
|
+
if (target.humanFeedback && clearPendingFix) target.humanFeedback.pendingFix = false;
|
|
1446
1466
|
target.minionsReview = { ...target.minionsReview, note: 'Fixed human feedback, awaiting re-review', fixedAt: ts() };
|
|
1447
|
-
|
|
1467
|
+
if (clearPendingFix) {
|
|
1468
|
+
log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
|
|
1469
|
+
} else {
|
|
1470
|
+
log('info', `Updated ${pr.id} → preserved newer humanFeedback.pendingFix, reset to waiting for re-review`);
|
|
1471
|
+
}
|
|
1448
1472
|
} else {
|
|
1449
1473
|
target.minionsReview = { ...target.minionsReview, note: 'Fixed, awaiting re-review', fixedAt: ts() };
|
|
1450
1474
|
log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
|
|
1451
1475
|
}
|
|
1476
|
+
if (automationCauseKey) {
|
|
1477
|
+
shared.markPrAutomationCause(target, automationCauseKey, {
|
|
1478
|
+
source,
|
|
1479
|
+
dispatchId: dispatchId || null,
|
|
1480
|
+
status: 'handled',
|
|
1481
|
+
handledAt: ts(),
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1452
1484
|
return prs;
|
|
1453
1485
|
}, { defaultValue: [] });
|
|
1454
1486
|
}
|
|
@@ -2762,7 +2794,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2762
2794
|
log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
|
|
2763
2795
|
}
|
|
2764
2796
|
if (type === WORK_TYPE.FIX && effectiveSuccess) {
|
|
2765
|
-
updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
|
|
2797
|
+
updatePrAfterFix(meta?.pr, meta?.project, meta?.source, meta?.automationCauseKey, dispatchItem?.id);
|
|
2766
2798
|
// (#984) Sync PRD status for PR-linked features: fix work items have a different ID
|
|
2767
2799
|
// than the original PRD feature, so syncPrdItemStatus(fixWiId, ...) finds nothing.
|
|
2768
2800
|
// Use the PR's prdItems to propagate done status when the original work item is done.
|
package/engine/shared.js
CHANGED
|
@@ -2532,6 +2532,40 @@ function safeSlugComponent(text, maxLen = 80) {
|
|
|
2532
2532
|
return `${base}-${hash}`.slice(0, maxLen);
|
|
2533
2533
|
}
|
|
2534
2534
|
|
|
2535
|
+
const PR_AUTOMATION_CAUSE_LIMIT = 50;
|
|
2536
|
+
|
|
2537
|
+
function getPrAutomationCauses(pr) {
|
|
2538
|
+
const causes = pr?._automationFixCauses;
|
|
2539
|
+
return causes && typeof causes === 'object' && !Array.isArray(causes) ? causes : {};
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
function hasPrAutomationCause(pr, causeKey) {
|
|
2543
|
+
return !!(causeKey && getPrAutomationCauses(pr)[causeKey]);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function markPrAutomationCause(pr, causeKey, details = {}) {
|
|
2547
|
+
if (!pr || !causeKey) return false;
|
|
2548
|
+
const now = ts();
|
|
2549
|
+
const causes = { ...getPrAutomationCauses(pr) };
|
|
2550
|
+
causes[causeKey] = {
|
|
2551
|
+
...(causes[causeKey] || {}),
|
|
2552
|
+
...details,
|
|
2553
|
+
status: details.status || causes[causeKey]?.status || 'handled',
|
|
2554
|
+
updatedAt: now,
|
|
2555
|
+
};
|
|
2556
|
+
if (!causes[causeKey].firstSeenAt) causes[causeKey].firstSeenAt = now;
|
|
2557
|
+
|
|
2558
|
+
const entries = Object.entries(causes);
|
|
2559
|
+
if (entries.length > PR_AUTOMATION_CAUSE_LIMIT) {
|
|
2560
|
+
entries
|
|
2561
|
+
.sort((a, b) => String(b[1]?.updatedAt || b[1]?.firstSeenAt || '').localeCompare(String(a[1]?.updatedAt || a[1]?.firstSeenAt || '')))
|
|
2562
|
+
.slice(PR_AUTOMATION_CAUSE_LIMIT)
|
|
2563
|
+
.forEach(([key]) => delete causes[key]);
|
|
2564
|
+
}
|
|
2565
|
+
pr._automationFixCauses = causes;
|
|
2566
|
+
return true;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2535
2569
|
function formatTranscriptEntry(t) {
|
|
2536
2570
|
return '### ' + (t.agent || 'agent') + ' (' + (t.type || '') + ', Round ' + (t.round || '?') + ')\n\n' + (t.content || '');
|
|
2537
2571
|
}
|
|
@@ -2716,6 +2750,9 @@ module.exports = {
|
|
|
2716
2750
|
redactSecrets,
|
|
2717
2751
|
slugify,
|
|
2718
2752
|
safeSlugComponent,
|
|
2753
|
+
getPrAutomationCauses,
|
|
2754
|
+
hasPrAutomationCause,
|
|
2755
|
+
markPrAutomationCause,
|
|
2719
2756
|
formatTranscriptEntry,
|
|
2720
2757
|
getPinnedItems,
|
|
2721
2758
|
_logBuffer, // exported for testing
|
package/engine.js
CHANGED
|
@@ -2308,6 +2308,93 @@ function ensurePrBranchForDispatch(project, pr, automationType) {
|
|
|
2308
2308
|
return '';
|
|
2309
2309
|
}
|
|
2310
2310
|
|
|
2311
|
+
function prCausePart(value, fallback = 'unknown') {
|
|
2312
|
+
const raw = String(value || '').trim();
|
|
2313
|
+
return shared.safeSlugComponent(raw || fallback, 80);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function getPrCauseHead(pr) {
|
|
2317
|
+
return pr?.headSha
|
|
2318
|
+
|| pr?.headSHA
|
|
2319
|
+
|| pr?.head?.sha
|
|
2320
|
+
|| pr?._adoSourceCommit
|
|
2321
|
+
|| pr?._adoHeadCommit
|
|
2322
|
+
|| pr?.lastMergeSourceCommit?.commitId
|
|
2323
|
+
|| pr?.lastMergeCommit?.commitId
|
|
2324
|
+
|| '';
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
function getPrCauseBase(pr) {
|
|
2328
|
+
return pr?.baseSha
|
|
2329
|
+
|| pr?.base?.sha
|
|
2330
|
+
|| pr?._adoTargetCommit
|
|
2331
|
+
|| pr?.lastMergeTargetCommit?.commitId
|
|
2332
|
+
|| pr?.targetSha
|
|
2333
|
+
|| pr?.targetRefSha
|
|
2334
|
+
|| pr?.baseRefName
|
|
2335
|
+
|| pr?.targetRefName
|
|
2336
|
+
|| '';
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
function getPrAutomationCauseKey(kind, pr) {
|
|
2340
|
+
if (kind === 'review-feedback') {
|
|
2341
|
+
const reviewRef = pr?.minionsReview?.dispatchId
|
|
2342
|
+
|| pr?.minionsReview?.reviewedAt
|
|
2343
|
+
|| pr?.lastReviewedAt
|
|
2344
|
+
|| getPrCauseHead(pr)
|
|
2345
|
+
|| pr?.reviewNote
|
|
2346
|
+
|| pr?.minionsReview?.note
|
|
2347
|
+
|| 'review';
|
|
2348
|
+
return `review-feedback:${prCausePart(reviewRef)}`;
|
|
2349
|
+
}
|
|
2350
|
+
if (kind === 'human-comment') {
|
|
2351
|
+
const commentRef = pr?.humanFeedback?.lastProcessedCommentKey
|
|
2352
|
+
|| pr?.humanFeedback?.lastProcessedCommentId
|
|
2353
|
+
|| pr?.humanFeedback?.commentId
|
|
2354
|
+
|| pr?.humanFeedback?.lastProcessedCommentDate
|
|
2355
|
+
|| pr?.humanFeedback?.feedbackContent
|
|
2356
|
+
|| 'comment';
|
|
2357
|
+
return `human-comment:${prCausePart(commentRef)}`;
|
|
2358
|
+
}
|
|
2359
|
+
if (kind === 'build') {
|
|
2360
|
+
const checkName = pr?.buildFailReason || pr?._buildStatusDetail || 'check';
|
|
2361
|
+
const signature = pr?.buildFailureSignature
|
|
2362
|
+
|| pr?.buildErrorLog
|
|
2363
|
+
|| pr?._buildStatusDetail
|
|
2364
|
+
|| pr?.buildStatusDetail
|
|
2365
|
+
|| pr?.buildFailReason
|
|
2366
|
+
|| 'failure';
|
|
2367
|
+
return `build:${prCausePart(checkName)}:${prCausePart(getPrCauseHead(pr), 'unknown-head')}:${prCausePart(signature)}`;
|
|
2368
|
+
}
|
|
2369
|
+
if (kind === 'merge-conflict') {
|
|
2370
|
+
return `merge-conflict:${prCausePart(getPrCauseBase(pr), 'unknown-base')}:${prCausePart(getPrCauseHead(pr), 'unknown-head')}`;
|
|
2371
|
+
}
|
|
2372
|
+
return `${kind}:${prCausePart(getPrCauseHead(pr) || pr?.id || 'pr')}`;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
function getPrAutomationDispatchKey(baseKey, causeKey) {
|
|
2376
|
+
return `${baseKey}-${shared.safeSlugComponent(causeKey, 96)}`;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
function isPrAutomationCausePending(project, pr, causeKey) {
|
|
2380
|
+
if (!causeKey) return false;
|
|
2381
|
+
const prCanonicalId = shared.getCanonicalPrId(project, pr, pr?.url || '');
|
|
2382
|
+
const dispatch = getDispatch();
|
|
2383
|
+
return [...(dispatch.pending || []), ...(dispatch.active || [])].some(d => {
|
|
2384
|
+
if (d.meta?.automationCauseKey !== causeKey) return false;
|
|
2385
|
+
if (!prCanonicalId) return true;
|
|
2386
|
+
const dispatchProject = d.meta?.project?.name
|
|
2387
|
+
? (getProjects(getConfig()).find(p => p.name === d.meta.project.name) || d.meta.project)
|
|
2388
|
+
: (d.meta?.project || null);
|
|
2389
|
+
const dispatchPrId = shared.getCanonicalPrId(dispatchProject, d.meta?.pr, d.meta?.pr?.url || '');
|
|
2390
|
+
return !dispatchPrId || dispatchPrId === prCanonicalId;
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
function isPrAutomationCauseHandledOrPending(project, pr, causeKey) {
|
|
2395
|
+
return shared.hasPrAutomationCause(pr, causeKey) || isPrAutomationCausePending(project, pr, causeKey);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2311
2398
|
|
|
2312
2399
|
// Tracks per-process which silent-discovery warnings have already been logged
|
|
2313
2400
|
// so we don't spam the log every tick. Cleared on process exit (no need to
|
|
@@ -2448,10 +2535,13 @@ async function discoverFromPrs(config, project) {
|
|
|
2448
2535
|
|
|
2449
2536
|
// Fresh reviewer comments are actionable fixes, even while the PR is otherwise
|
|
2450
2537
|
// awaiting a stale-vote re-review or has build-fix retries escalated.
|
|
2451
|
-
const
|
|
2538
|
+
const humanFixBaseKey = `human-fix-${project?.name || 'default'}-${prDisplayId}`;
|
|
2539
|
+
const humanCauseKey = getPrAutomationCauseKey('human-comment', pr);
|
|
2540
|
+
const humanFixKey = getPrAutomationDispatchKey(humanFixBaseKey, humanCauseKey);
|
|
2452
2541
|
const hasCoalescedFeedback = (dispatchCooldowns.get(humanFixKey)?.pendingContexts || []).length > 0;
|
|
2453
2542
|
if (pollEnabled && autoFixHumanComments && (pr.humanFeedback?.pendingFix || hasCoalescedFeedback) && !fixDispatched) {
|
|
2454
2543
|
const key = humanFixKey;
|
|
2544
|
+
if (isPrAutomationCauseHandledOrPending(project, pr, humanCauseKey)) continue;
|
|
2455
2545
|
let staleCoalesced = [];
|
|
2456
2546
|
const alreadyDispatched = isAlreadyDispatched(key);
|
|
2457
2547
|
const blockedByCooldown = isOnCooldown(key, cooldownMs);
|
|
@@ -2492,7 +2582,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2492
2582
|
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: prBranch,
|
|
2493
2583
|
reviewer: 'Human Reviewer',
|
|
2494
2584
|
review_note: reviewNote,
|
|
2495
|
-
}, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, source: 'pr-human-feedback', pr, branch: prBranch, project: projMeta });
|
|
2585
|
+
}, `Fix ${pr.id}: ${pr.title || ''} — human feedback`, { dispatchKey: key, automationCauseKey: humanCauseKey, source: 'pr-human-feedback', pr, branch: prBranch, project: projMeta });
|
|
2496
2586
|
if (item) { newWork.push(item); fixDispatched = true; }
|
|
2497
2587
|
}
|
|
2498
2588
|
|
|
@@ -2547,7 +2637,9 @@ async function discoverFromPrs(config, project) {
|
|
|
2547
2637
|
// PRs with changes requested → route back to author for fix.
|
|
2548
2638
|
// Gate on evalLoopEnabled and provider polling — the review→fix cycle depends on fresh vote state.
|
|
2549
2639
|
if (evalLoopEnabled && pollEnabled && autoFixReviewFeedback && reviewStatus === 'changes-requested' && !awaitingReReview && !fixDispatched) {
|
|
2550
|
-
const
|
|
2640
|
+
const reviewCauseKey = getPrAutomationCauseKey('review-feedback', pr);
|
|
2641
|
+
const key = getPrAutomationDispatchKey(`fix-${project?.name || 'default'}-${prDisplayId}`, reviewCauseKey);
|
|
2642
|
+
if (isPrAutomationCauseHandledOrPending(project, pr, reviewCauseKey)) continue;
|
|
2551
2643
|
if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2552
2644
|
const agentId = resolveAgent('fix', config, { authorAgent: pr.agent });
|
|
2553
2645
|
if (!agentId) continue;
|
|
@@ -2557,7 +2649,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2557
2649
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2558
2650
|
pr_id: pr.id, pr_branch: prBranch,
|
|
2559
2651
|
review_note: pr.minionsReview?.note || pr.reviewNote || 'See PR thread comments',
|
|
2560
|
-
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2652
|
+
}, `Fix ${pr.id}: ${pr.title || ''} — review feedback`, { dispatchKey: key, automationCauseKey: reviewCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2561
2653
|
if (item) {
|
|
2562
2654
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2563
2655
|
}
|
|
@@ -2572,7 +2664,9 @@ async function discoverFromPrs(config, project) {
|
|
|
2572
2664
|
}
|
|
2573
2665
|
const autoFixBuilds = config.engine?.autoFixBuilds ?? ENGINE_DEFAULTS.autoFixBuilds;
|
|
2574
2666
|
if (pollEnabled && autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing') {
|
|
2575
|
-
const
|
|
2667
|
+
const buildCauseKey = getPrAutomationCauseKey('build', pr);
|
|
2668
|
+
const key = getPrAutomationDispatchKey(`build-fix-${project?.name || 'default'}-${prDisplayId}`, buildCauseKey);
|
|
2669
|
+
if (isPrAutomationCauseHandledOrPending(project, pr, buildCauseKey)) continue;
|
|
2576
2670
|
if (fixThrottled || isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2577
2671
|
|
|
2578
2672
|
// Pre-dispatch live build check — cached buildStatus may be stale: ADO can
|
|
@@ -2633,7 +2727,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2633
2727
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2634
2728
|
pr_id: pr.id, pr_branch: prBranch,
|
|
2635
2729
|
review_note: reviewNote,
|
|
2636
|
-
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2730
|
+
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, automationCauseKey: buildCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2637
2731
|
if (item) {
|
|
2638
2732
|
newWork.push(item); setCooldown(key); fixDispatched = true;
|
|
2639
2733
|
try {
|
|
@@ -2664,13 +2758,14 @@ async function discoverFromPrs(config, project) {
|
|
|
2664
2758
|
// PRs with merge conflicts — dispatch fix to resolve (gated by provider polling + autoFixConflicts)
|
|
2665
2759
|
const autoFixConflicts = config.engine?.autoFixConflicts ?? ENGINE_DEFAULTS.autoFixConflicts;
|
|
2666
2760
|
if (pollEnabled && autoFixConflicts && pr.status === PR_STATUS.ACTIVE && pr._mergeConflict && !fixDispatched) {
|
|
2667
|
-
const
|
|
2761
|
+
const conflictCauseKey = getPrAutomationCauseKey('merge-conflict', pr);
|
|
2762
|
+
const key = getPrAutomationDispatchKey(`conflict-fix-${project?.name || 'default'}-${prDisplayId}`, conflictCauseKey);
|
|
2668
2763
|
// Suppress re-dispatch for 10 min after last attempt — ADO/GitHub recomputes
|
|
2669
2764
|
// mergeStatus asynchronously (1–5 min lag), so the flag may stay set even after
|
|
2670
2765
|
// a successful push. _conflictFixedAt is cleared when the poller confirms clean status.
|
|
2671
2766
|
const conflictFixedAt = pr._conflictFixedAt;
|
|
2672
2767
|
const withinLag = conflictFixedAt && Date.now() - new Date(conflictFixedAt).getTime() < 10 * 60 * 1000;
|
|
2673
|
-
if (!withinLag && !fixThrottled && !isAlreadyDispatched(key) && !isOnCooldown(key, cooldownMs)) {
|
|
2768
|
+
if (!withinLag && !fixThrottled && !isPrAutomationCauseHandledOrPending(project, pr, conflictCauseKey) && !isAlreadyDispatched(key) && !isOnCooldown(key, cooldownMs)) {
|
|
2674
2769
|
// Pre-dispatch live conflict check — cached `_mergeConflict` may be
|
|
2675
2770
|
// stale: ADO/GitHub recompute mergeStatus asynchronously (1–5 min lag),
|
|
2676
2771
|
// so a successful upstream merge can leave the flag set even after the
|
|
@@ -2702,7 +2797,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2702
2797
|
const item = buildPrDispatch(agentId, config, project, pr, 'fix', {
|
|
2703
2798
|
pr_id: pr.id, pr_branch: prBranch,
|
|
2704
2799
|
review_note: `This PR has merge conflicts with the target branch. Inspect the live PR and repository history, choose the safest merge/rebase/update strategy, resolve all conflicts, validate the result, and push the branch.`,
|
|
2705
|
-
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2800
|
+
}, `Fix merge conflicts on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, automationCauseKey: conflictCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
2706
2801
|
if (item) {
|
|
2707
2802
|
newWork.push(item);
|
|
2708
2803
|
setCooldown(key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1725",
|
|
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"
|