@yemi33/minions 0.1.2312 → 0.1.2314
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/dashboard/js/refresh.js +5 -2
- package/dashboard/js/render-other.js +50 -5
- package/dashboard/js/render-prs.js +24 -0
- package/dashboard/js/render-work-items.js +7 -2
- package/dashboard/styles.css +3 -0
- package/dashboard.js +14 -7
- package/engine/cleanup.js +2 -1
- package/engine/lifecycle.js +167 -0
- package/engine/queries.js +2 -2
- package/engine/shared.js +81 -3
- package/engine.js +16 -2
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -136,7 +136,8 @@ const RENDER_VERSIONS = {
|
|
|
136
136
|
inbox: 2,
|
|
137
137
|
// Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
|
|
138
138
|
// Bumped 5→6 for multi-select hybrid liveValidation.type support (W-mr2m1ute000a9c01).
|
|
139
|
-
|
|
139
|
+
// Bumped 6→7 for the liveValidation.autoDispatch toggle + pill "· auto-validate" suffix (W-mr2q361a00097e5c).
|
|
140
|
+
projects: 7,
|
|
140
141
|
notes: 1,
|
|
141
142
|
prd: 3,
|
|
142
143
|
prs: 3,
|
|
@@ -149,7 +150,9 @@ const RENDER_VERSIONS = {
|
|
|
149
150
|
dispatch: 2,
|
|
150
151
|
engineLog: 2,
|
|
151
152
|
metrics: 1,
|
|
152
|
-
|
|
153
|
+
// Bumped 7→8: WI detail modal's branch pill moved out of the Artifacts
|
|
154
|
+
// chip row into its own non-interactive "Branch" field (W-mr2qjr3b0002522f).
|
|
155
|
+
workItems: 8,
|
|
153
156
|
skills: 1,
|
|
154
157
|
commands: 1,
|
|
155
158
|
mcpServers: 1,
|
|
@@ -112,7 +112,15 @@ function _renderWorktreeModePill(p) {
|
|
|
112
112
|
if (types.length > 0) {
|
|
113
113
|
const typesText = types.join(', ');
|
|
114
114
|
const vt = escapeHtml(typesText);
|
|
115
|
-
|
|
115
|
+
// W-mr2q361a00097e5c — visibly distinguish "Hybrid (manual validation)"
|
|
116
|
+
// from "Hybrid (auto-validate)" so the operator doesn't have to reopen
|
|
117
|
+
// the picker to discover whether autoDispatch is silently off.
|
|
118
|
+
const autoDispatch = p.liveValidationAutoDispatch === true;
|
|
119
|
+
const autoSuffix = autoDispatch ? ' · auto-validate' : '';
|
|
120
|
+
const autoTitleBit = autoDispatch
|
|
121
|
+
? ' A validation work item is automatically dispatched after each coding work item completes.'
|
|
122
|
+
: ' Auto-validation is OFF — validation must be dispatched manually after each coding work item completes.';
|
|
123
|
+
return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid live-validation — coding work items author in isolated worktrees; only the "' + vt + '" validation type(s) run in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree).' + escHtml(autoTitleBit) + ' Click to change.">⚡ Hybrid · ' + vt + escHtml(autoSuffix) + '</span>';
|
|
116
124
|
}
|
|
117
125
|
return ' <span class="project-mode-pill project-mode-live project-mode-pill-clickable"' + common + ' title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree. Click to change.">⚡ Live checkout</span>';
|
|
118
126
|
}
|
|
@@ -276,6 +284,24 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
276
284
|
});
|
|
277
285
|
menu.appendChild(checkboxList);
|
|
278
286
|
|
|
287
|
+
// W-mr2q361a00097e5c — "auto-validate" toggle for liveValidation.autoDispatch.
|
|
288
|
+
// Default UNCHECKED to match today's implicit default (autoDispatch absent
|
|
289
|
+
// / false) so reopening the picker on a project that has never set this
|
|
290
|
+
// never silently flips it on; preselect from the project's CURRENT value
|
|
291
|
+
// when one is already configured (mirrors the type-checkbox preselect
|
|
292
|
+
// above) so reopening an already-hybrid project reflects its real state.
|
|
293
|
+
const autoDispatchRow = document.createElement('label');
|
|
294
|
+
autoDispatchRow.className = 'checkout-mode-menu-checkbox-row checkout-mode-menu-autodispatch-row';
|
|
295
|
+
const autoDispatchCb = document.createElement('input');
|
|
296
|
+
autoDispatchCb.type = 'checkbox';
|
|
297
|
+
autoDispatchCb.className = 'checkout-mode-menu-checkbox checkout-mode-menu-autodispatch-checkbox';
|
|
298
|
+
autoDispatchCb.checked = project.liveValidationAutoDispatch === true;
|
|
299
|
+
const autoDispatchText = document.createElement('span');
|
|
300
|
+
autoDispatchText.textContent = 'Automatically validate after each coding work item completes';
|
|
301
|
+
autoDispatchRow.appendChild(autoDispatchCb);
|
|
302
|
+
autoDispatchRow.appendChild(autoDispatchText);
|
|
303
|
+
menu.appendChild(autoDispatchRow);
|
|
304
|
+
|
|
279
305
|
const actions = document.createElement('div');
|
|
280
306
|
actions.className = 'checkout-mode-menu-actions';
|
|
281
307
|
|
|
@@ -297,8 +323,9 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
297
323
|
applyBtn.addEventListener('click', function() {
|
|
298
324
|
const types = checkboxes.filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
|
|
299
325
|
if (types.length === 0) return; // require at least one selected type
|
|
326
|
+
const autoDispatch = autoDispatchCb.checked;
|
|
300
327
|
_closeCheckoutModeMenu();
|
|
301
|
-
_applyCheckoutModeChange(projectName, 'live', types);
|
|
328
|
+
_applyCheckoutModeChange(projectName, 'live', types, autoDispatch);
|
|
302
329
|
});
|
|
303
330
|
actions.appendChild(applyBtn);
|
|
304
331
|
|
|
@@ -316,18 +343,33 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
316
343
|
// whichever shape is passed through is what gets POSTed as
|
|
317
344
|
// liveValidation.type, so shared.validateLiveValidation on the server decides
|
|
318
345
|
// the final persisted shape.
|
|
319
|
-
|
|
346
|
+
//
|
|
347
|
+
// `autoDispatch` (W-mr2q361a00097e5c, optional, only meaningful for hybrid
|
|
348
|
+
// mode) threads the "auto-validate" checkbox value through to the POST body
|
|
349
|
+
// as liveValidation.autoDispatch. Non-hybrid callers (Worktrees / full Live
|
|
350
|
+
// checkout) omit it, which correctly clears autoDispatch when leaving hybrid
|
|
351
|
+
// mode (liveValidation is null entirely in that case).
|
|
352
|
+
async function _applyCheckoutModeChange(projectName, newMode, liveValidationType, autoDispatch) {
|
|
320
353
|
const project = _findProjectInLastStatus(projectName);
|
|
321
354
|
if (!project) return;
|
|
322
355
|
const typesForLabel = liveValidationType
|
|
323
356
|
? (Array.isArray(liveValidationType) ? liveValidationType : [liveValidationType])
|
|
324
357
|
: [];
|
|
358
|
+
const autoDispatchOn = typesForLabel.length > 0 && !!autoDispatch;
|
|
325
359
|
const label = newMode === 'worktree'
|
|
326
360
|
? 'Worktrees'
|
|
327
361
|
: (typesForLabel.length > 0 ? ('Hybrid (' + typesForLabel.join(', ') + ')') : 'Live checkout');
|
|
362
|
+
// W-mr2q361a00097e5c — auto-validation is a workflow-changing decision
|
|
363
|
+
// (agents stop running local builds once it's on), so call it out
|
|
364
|
+
// explicitly in the confirm message rather than burying it in the label.
|
|
365
|
+
const autoDispatchNote = typesForLabel.length > 0
|
|
366
|
+
? (autoDispatchOn
|
|
367
|
+
? ' Auto-validation will be turned ON — a validation work item will be dispatched automatically after each coding work item completes, and coding agents will skip local builds/tests.'
|
|
368
|
+
: ' Auto-validation will be OFF — you will need to dispatch validation manually after each coding work item completes.')
|
|
369
|
+
: '';
|
|
328
370
|
const ok = await confirmDialog({
|
|
329
371
|
title: 'Change checkout mode?',
|
|
330
|
-
message: 'Switch "' + projectName + '" to ' + label + '? This changes how every future dispatch on this project runs.',
|
|
372
|
+
message: 'Switch "' + projectName + '" to ' + label + '?' + autoDispatchNote + ' This changes how every future dispatch on this project runs.',
|
|
331
373
|
confirmLabel: 'Change mode',
|
|
332
374
|
cancelLabel: 'Cancel',
|
|
333
375
|
danger: newMode !== 'worktree',
|
|
@@ -336,11 +378,13 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
|
|
|
336
378
|
|
|
337
379
|
const prevMode = project.checkoutMode;
|
|
338
380
|
const prevType = project.liveValidationType;
|
|
381
|
+
const prevAutoDispatch = project.liveValidationAutoDispatch;
|
|
339
382
|
const nextType = (newMode === 'live' && liveValidationType) ? liveValidationType : null;
|
|
340
383
|
// Optimistic flip BEFORE awaiting the API call (per dashboard convention —
|
|
341
384
|
// see projectChipRemove / removePinnedNote); reverted in the catch below.
|
|
342
385
|
project.checkoutMode = newMode;
|
|
343
386
|
project.liveValidationType = nextType;
|
|
387
|
+
project.liveValidationAutoDispatch = nextType ? autoDispatchOn : false;
|
|
344
388
|
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
345
389
|
|
|
346
390
|
try {
|
|
@@ -351,7 +395,7 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
|
|
|
351
395
|
projects: [{
|
|
352
396
|
name: projectName,
|
|
353
397
|
checkoutMode: newMode,
|
|
354
|
-
liveValidation: nextType ? { type: nextType } : null,
|
|
398
|
+
liveValidation: nextType ? { type: nextType, autoDispatch: autoDispatchOn } : null,
|
|
355
399
|
}],
|
|
356
400
|
}),
|
|
357
401
|
});
|
|
@@ -361,6 +405,7 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
|
|
|
361
405
|
} catch (err) {
|
|
362
406
|
project.checkoutMode = prevMode;
|
|
363
407
|
project.liveValidationType = prevType;
|
|
408
|
+
project.liveValidationAutoDispatch = prevAutoDispatch;
|
|
364
409
|
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
365
410
|
showToast('cmd-toast', 'Failed to update checkout mode: ' + (err && err.message || err), false);
|
|
366
411
|
}
|
|
@@ -187,6 +187,30 @@ function prRow(pr) {
|
|
|
187
187
|
+ '⏸ paused: ' + escapeHtml(cause) + ' ▶</button>';
|
|
188
188
|
}).join('');
|
|
189
189
|
}
|
|
190
|
+
// #639 follow-up — build-fix-ineffective chip. Distinct from the generic
|
|
191
|
+
// no-op pausedCauses above: pr._buildFixIneffective is set when a
|
|
192
|
+
// BUILD_FAILURE fix reported "no branch change" AND a live CI re-poll
|
|
193
|
+
// confirmed the build is STILL failing on that same head (see
|
|
194
|
+
// engine/lifecycle.js recordBuildFixIneffective). This is a separate,
|
|
195
|
+
// headSha-keyed pause distinct from _noOpFixes, so it needs its own,
|
|
196
|
+
// clearly-labeled indicator — a still-red build after an unchanged fix
|
|
197
|
+
// attempt usually means the failure isn't fixable by another agent pass
|
|
198
|
+
// (flaky infra, external outage, etc.), hence "infra issue suspected".
|
|
199
|
+
// No resume action is wired here (clearing this record is an engine-side
|
|
200
|
+
// change out of scope for this UI-only follow-up) — informational only.
|
|
201
|
+
var buildFixIneffective = pr._buildFixIneffective;
|
|
202
|
+
if (buildFixIneffective && typeof buildFixIneffective === 'object' && buildFixIneffective.paused === true) {
|
|
203
|
+
var bfiTitle = 'Build-fix reported no branch change, and a live CI re-poll confirmed the build is still "'
|
|
204
|
+
+ (buildFixIneffective.liveBuildStatus || 'failing') + '" on the same commit ('
|
|
205
|
+
+ (buildFixIneffective.headSha || 'unknown head') + ') after '
|
|
206
|
+
+ (buildFixIneffective.count || '?') + ' ineffective attempt(s). Reason: '
|
|
207
|
+
+ (buildFixIneffective.reason || 'unknown')
|
|
208
|
+
+ '. A new push to this branch will reset this pause.';
|
|
209
|
+
pausedChips += ' <span class="pr-badge rejected pr-build-fix-ineffective-chip" '
|
|
210
|
+
+ 'style="font-size:var(--text-xs)" '
|
|
211
|
+
+ 'title="' + escapeHtml(bfiTitle) + '">'
|
|
212
|
+
+ '⏸ infra issue suspected ▶</span>';
|
|
213
|
+
}
|
|
190
214
|
const titleText = pr.title || 'Untitled';
|
|
191
215
|
// Polish (W-mqv5ccn7): flatten Markdown in the body so the preview shows
|
|
192
216
|
// plain text instead of raw `##` / `**…**` / backtick source.
|
|
@@ -889,8 +889,6 @@ function _wiRenderDetail(item) {
|
|
|
889
889
|
// chip is needed.
|
|
890
890
|
var arts = item._artifacts || {};
|
|
891
891
|
var artPills = '';
|
|
892
|
-
var branchStyle = 'display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:var(--text-sm);background:var(--surface2);border:1px solid var(--border);color:var(--text);cursor:default';
|
|
893
|
-
if (arts.branch) artPills += '<span style="' + branchStyle + '">🌿 ' + escapeHtml(arts.branch) + '</span> ';
|
|
894
892
|
if (arts.plan) artPills += renderArtifactLink({ type: 'plan', id: arts.plan, label: 'Plan' }) + ' ';
|
|
895
893
|
if (arts.prd) artPills += renderArtifactLink({ type: 'prd', id: arts.prd, label: 'PRD' }) + ' ';
|
|
896
894
|
if (arts.sourcePlan) artPills += renderArtifactLink({ type: 'plan', id: arts.sourcePlan, label: 'Source Plan' }) + ' ';
|
|
@@ -915,6 +913,13 @@ function _wiRenderDetail(item) {
|
|
|
915
913
|
});
|
|
916
914
|
}
|
|
917
915
|
if (artPills) html += field('Artifacts', '<div style="display:flex;flex-wrap:wrap;gap:4px">' + artPills + '</div>');
|
|
916
|
+
// Branch is not a clickable artifact link (no navigation target), so it is
|
|
917
|
+
// rendered as its own plain-text field rather than sharing the Artifacts
|
|
918
|
+
// pill row's chip styling — matches render-prs.js's PR-modal "Branch:"
|
|
919
|
+
// field (dashboard/js/render-prs.js:517), which uses a plain <code> tag
|
|
920
|
+
// instead of the clickable-chip pill style (issue: branch pill looked like
|
|
921
|
+
// a dead artifact link).
|
|
922
|
+
if (arts.branch) html += field('Branch', '🌿 <code style="font-size:var(--text-sm);background:var(--surface2);padding:2px 6px;border-radius:var(--radius-sm)">' + escapeHtml(arts.branch) + '</code>');
|
|
918
923
|
|
|
919
924
|
// P-d5a6f7c4 (Harness Transparency, Stage 3 — surface). Render the harness
|
|
920
925
|
// usage the agent self-reported. Shape:
|
package/dashboard/styles.css
CHANGED
|
@@ -1341,6 +1341,9 @@
|
|
|
1341
1341
|
}
|
|
1342
1342
|
.checkout-mode-menu-checkbox-row:hover { background: var(--surface); }
|
|
1343
1343
|
.checkout-mode-menu-checkbox { cursor: pointer; }
|
|
1344
|
+
.checkout-mode-menu-autodispatch-row {
|
|
1345
|
+
margin-top: 6px; padding-top: 10px; border-top: 1px solid var(--border);
|
|
1346
|
+
}
|
|
1344
1347
|
.checkout-mode-menu-actions {
|
|
1345
1348
|
display: flex; justify-content: flex-end; gap: 6px; padding-top: 4px;
|
|
1346
1349
|
}
|
package/dashboard.js
CHANGED
|
@@ -1156,16 +1156,16 @@ function findWorkItemsTargetById(id, source, projects = PROJECTS) {
|
|
|
1156
1156
|
if (explicitSource) {
|
|
1157
1157
|
const target = resolveProjectSourceTarget(source, projects);
|
|
1158
1158
|
if (target.error) return { error: target.error };
|
|
1159
|
-
const items = shared.
|
|
1159
|
+
const items = shared.safeJsonArr(target.wiPath);
|
|
1160
1160
|
return { ...target, found: items.some(i => i.id === id) };
|
|
1161
1161
|
}
|
|
1162
1162
|
|
|
1163
1163
|
const central = resolveProjectSourceTarget('central', projects);
|
|
1164
|
-
const centralItems = shared.
|
|
1164
|
+
const centralItems = shared.safeJsonArr(central.wiPath);
|
|
1165
1165
|
if (centralItems.some(i => i.id === id)) return { ...central, found: true };
|
|
1166
1166
|
for (const project of projects) {
|
|
1167
1167
|
const target = resolveProjectSourceTarget(project.name, projects);
|
|
1168
|
-
const items = shared.
|
|
1168
|
+
const items = shared.safeJsonArr(target.wiPath);
|
|
1169
1169
|
if (items.some(i => i.id === id)) return { ...target, found: true };
|
|
1170
1170
|
}
|
|
1171
1171
|
return { found: false };
|
|
@@ -2769,6 +2769,13 @@ function _buildStatusSlowState() {
|
|
|
2769
2769
|
liveValidationType: (shared.resolveCheckoutMode(p) === 'live' && p.liveValidation && p.liveValidation.type)
|
|
2770
2770
|
? p.liveValidation.type
|
|
2771
2771
|
: null,
|
|
2772
|
+
// W-mr2q361a00097e5c — surface liveValidation.autoDispatch alongside
|
|
2773
|
+
// .type so the hybrid picker can preselect the "auto-validate"
|
|
2774
|
+
// checkbox and the pill can visibly distinguish manual vs
|
|
2775
|
+
// auto-validating hybrid projects. Only meaningful when checkoutMode
|
|
2776
|
+
// is 'live'; false otherwise (matches the implicit today-default of
|
|
2777
|
+
// autoDispatch being absent/false).
|
|
2778
|
+
liveValidationAutoDispatch: !!(shared.resolveCheckoutMode(p) === 'live' && p.liveValidation && p.liveValidation.autoDispatch === true),
|
|
2772
2779
|
};
|
|
2773
2780
|
}),
|
|
2774
2781
|
autoMode: {
|
|
@@ -8069,7 +8076,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
8069
8076
|
const config = queries.getConfig();
|
|
8070
8077
|
const allWorkItems = queries.getWorkItems(config);
|
|
8071
8078
|
const planWis = allWorkItems.filter(w => w.sourcePlan === body.file && w.itemType !== 'pr' && w.itemType !== 'verify');
|
|
8072
|
-
const allPrs = PROJECTS.flatMap(p => shared.
|
|
8079
|
+
const allPrs = PROJECTS.flatMap(p => shared.safeJsonArr(shared.projectPrPath(p)));
|
|
8073
8080
|
const prLinks = shared.getPrLinks();
|
|
8074
8081
|
const implContext = (plan.missing_features || []).map(f => {
|
|
8075
8082
|
const wi = planWis.find(w => w.id === f.id);
|
|
@@ -11004,7 +11011,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
11004
11011
|
async function handleSettingsUpdate(req, res) {
|
|
11005
11012
|
try {
|
|
11006
11013
|
const body = await readBody(req);
|
|
11007
|
-
const config =
|
|
11014
|
+
const config = safeJsonObj(CONFIG_PATH);
|
|
11008
11015
|
if (!config.engine) config.engine = {};
|
|
11009
11016
|
if (!config.agents) config.agents = {};
|
|
11010
11017
|
shared.pruneDefaultClaudeConfig(config);
|
|
@@ -13298,7 +13305,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13298
13305
|
// (Round-2 review finding #3.)
|
|
13299
13306
|
const cfg = queries.getConfig() || {};
|
|
13300
13307
|
const scheds = cfg.schedules || [];
|
|
13301
|
-
const runs = shared.
|
|
13308
|
+
const runs = shared.safeJsonObj(path.join(ENGINE_DIR, 'schedule-runs.json'));
|
|
13302
13309
|
return scheds.map(s => {
|
|
13303
13310
|
const runEntry = runs[s.id];
|
|
13304
13311
|
const _lastRun = typeof runEntry === 'string' ? runEntry : (runEntry?.lastRun || runEntry?.lastCompletedAt || null);
|
|
@@ -13507,7 +13514,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13507
13514
|
|
|
13508
13515
|
// KB pin state (server-side so CC can pin items)
|
|
13509
13516
|
{ method: 'GET', path: '/api/kb-pins', desc: 'Get pinned KB item keys', handler: async (req, res) => {
|
|
13510
|
-
const pins = shared.
|
|
13517
|
+
const pins = shared.safeJsonArr(KB_PINS_PATH);
|
|
13511
13518
|
return jsonReply(res, 200, { pins });
|
|
13512
13519
|
}},
|
|
13513
13520
|
{ method: 'POST', path: '/api/kb-pins', desc: 'Set pinned KB item keys', params: 'pins[]', handler: async (req, res) => {
|
package/engine/cleanup.js
CHANGED
|
@@ -736,7 +736,8 @@ async function runCleanup(config, verbose = false) {
|
|
|
736
736
|
// claims it.
|
|
737
737
|
if (shared.isWorktreePathLive(full)) {
|
|
738
738
|
log('info', `Cleanup: skip orphan worktree dir ${full} — live dispatch claims it`);
|
|
739
|
-
shared.
|
|
739
|
+
const blockingInfo = shared.getWorktreeBlockingDispatchInfo(full);
|
|
740
|
+
shared._writeWorktreeSkipLiveInboxNote(full, 'cleanup.orphanWorktreeDirSweep', blockingInfo);
|
|
740
741
|
continue;
|
|
741
742
|
}
|
|
742
743
|
try {
|
package/engine/lifecycle.js
CHANGED
|
@@ -2647,6 +2647,122 @@ async function detectPrFixBranchChange(meta, config) {
|
|
|
2647
2647
|
return { changed: null, beforeHead, afterHead: remoteHead || '', reason: 'unable to prove branch head after fix' };
|
|
2648
2648
|
}
|
|
2649
2649
|
|
|
2650
|
+
// #639 — lazy requires for the host-specific live build/conflict checkers.
|
|
2651
|
+
// engine/github.js and engine/ado.js both lazy-require lifecycle.js (see
|
|
2652
|
+
// their `lifecycleModule()` helpers), so a top-level require here would form
|
|
2653
|
+
// an import cycle. Deferring to first call avoids that.
|
|
2654
|
+
let _githubPrModule = null;
|
|
2655
|
+
function githubPrModule() { if (!_githubPrModule) _githubPrModule = require('./github'); return _githubPrModule; }
|
|
2656
|
+
let _adoPrModule = null;
|
|
2657
|
+
function adoPrModule() { if (!_adoPrModule) _adoPrModule = require('./ado'); return _adoPrModule; }
|
|
2658
|
+
|
|
2659
|
+
// #639 — No-op fix detection previously only proved whether the agent pushed
|
|
2660
|
+
// a NEW commit; it never checked whether the build the fix was dispatched
|
|
2661
|
+
// against had actually turned green. That let a stuck failure get marked
|
|
2662
|
+
// "handled"/no-op forever: a fix commit lands and gets recorded as handled,
|
|
2663
|
+
// CI re-runs and fails again with the SAME signature against that exact
|
|
2664
|
+
// commit, and every subsequent build-fix dispatch finds "the fix is already
|
|
2665
|
+
// on the branch" (branch unchanged) and concludes no-op — without ever
|
|
2666
|
+
// re-checking that the build is still red. Call this right before accepting
|
|
2667
|
+
// a BUILD_FAILURE "no branch change" outcome as a clean no-op so the caller
|
|
2668
|
+
// can distinguish "genuinely nothing to do" from "fix was ineffective".
|
|
2669
|
+
async function checkBuildStillFailingLive(pr, project) {
|
|
2670
|
+
if (!pr || !project) return null;
|
|
2671
|
+
try {
|
|
2672
|
+
const checkFn = project.repoHost === 'github'
|
|
2673
|
+
? githubPrModule().checkLiveBuildAndConflict
|
|
2674
|
+
: adoPrModule().checkLiveBuildAndConflict;
|
|
2675
|
+
if (typeof checkFn !== 'function') return null;
|
|
2676
|
+
const live = await checkFn(pr, project);
|
|
2677
|
+
// A stale/errored live read carries no fresh evidence — don't let it
|
|
2678
|
+
// masquerade as a confirmed still-failing result.
|
|
2679
|
+
if (!live || live.buildStatusStale) return null;
|
|
2680
|
+
return live.buildStatus || null;
|
|
2681
|
+
} catch (err) {
|
|
2682
|
+
log('warn', `Live build re-check for ${pr.id || pr.url || '(unknown PR)'}: ${err.message}`);
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// #639 — Records a build-fix dispatch that reported "no branch change" AND
|
|
2688
|
+
// was verified (via live re-poll) to still be failing on the same head.
|
|
2689
|
+
// Tracked deliberately separately from `_noOpFixes[BUILD_FAILURE]` — see
|
|
2690
|
+
// `shared.isBuildFixIneffectivePaused` for why sharing that counter would
|
|
2691
|
+
// let a genuinely-broken build get stuck paused forever with no distinct
|
|
2692
|
+
// signal. The dispatch-eligibility gate for BUILD_FAILURE reads BOTH
|
|
2693
|
+
// `shared.isPrNoOpFixCausePaused` and `shared.isBuildFixIneffectivePaused`
|
|
2694
|
+
// (engine.js), and neither this record's `outcome` nor its pause state
|
|
2695
|
+
// suppresses the same-head skip guard tied to `outcome === 'noop'` — so the
|
|
2696
|
+
// engine keeps re-dispatching (subject to the normal cooldown) with a fresh
|
|
2697
|
+
// instruction to inspect the CURRENT failure rather than assume the prior
|
|
2698
|
+
// commit already fixed it, until either a real fix lands (head moves) or
|
|
2699
|
+
// the attempt cap is reached and a human is alerted.
|
|
2700
|
+
function recordBuildFixIneffective(target, dispatchItem, config, liveBuildStatus) {
|
|
2701
|
+
const headSha = getPrFixBaselineHead(target);
|
|
2702
|
+
const prior = target._buildFixIneffective && typeof target._buildFixIneffective === 'object'
|
|
2703
|
+
? target._buildFixIneffective : null;
|
|
2704
|
+
const sameHead = !!prior && !!headSha && prior.headSha === headSha;
|
|
2705
|
+
const count = sameHead ? (Number(prior.count) || 0) + 1 : 1;
|
|
2706
|
+
const pauseAfter = Number(config?.engine?.prNoOpFixPauseAttempts) || ENGINE_DEFAULTS.prNoOpFixPauseAttempts;
|
|
2707
|
+
const paused = count >= pauseAfter;
|
|
2708
|
+
const flippedToPaused = paused && !(sameHead && prior?.paused === true);
|
|
2709
|
+
const now = ts();
|
|
2710
|
+
// Human-teammate follow-up on #639/#640: once the automation is actually
|
|
2711
|
+
// PAUSED here (multiple ineffective attempts against the same unchanged
|
|
2712
|
+
// head, with live CI re-confirmed still red), a generic "no-op" label
|
|
2713
|
+
// undersells what's going on — repeated fixes aren't landing, which is a
|
|
2714
|
+
// much stronger signal of an environment/CI infra problem than a code
|
|
2715
|
+
// issue. `label` is the short, canonical diagnostic tag; it stays `null`
|
|
2716
|
+
// while attempts are still accumulating (not yet paused) so callers can
|
|
2717
|
+
// tell "still trying" apart from "give up on code fixes, check infra".
|
|
2718
|
+
// Surfaced on every consumer of this record: the PR record field itself
|
|
2719
|
+
// (`label`, plus `reason` below), the pause inbox alert, and the dashboard
|
|
2720
|
+
// chip (dashboard/js/render-prs.js already renders "infra issue suspected"
|
|
2721
|
+
// for `paused === true` — kept in sync with this same wording here).
|
|
2722
|
+
const label = paused ? 'infra issue suspected' : null;
|
|
2723
|
+
const shortHead = String(headSha || '').slice(0, 12);
|
|
2724
|
+
const reason = paused
|
|
2725
|
+
? `infra issue suspected — build-fix reported no branch change but live CI is still ${liveBuildStatus || 'failing'} on head ${shortHead} after ${count} consecutive ineffective attempt(s)`
|
|
2726
|
+
: `build-fix reported no branch change but live CI is still ${liveBuildStatus || 'failing'} on head ${shortHead}`;
|
|
2727
|
+
target._buildFixIneffective = {
|
|
2728
|
+
count,
|
|
2729
|
+
paused,
|
|
2730
|
+
label,
|
|
2731
|
+
headSha,
|
|
2732
|
+
liveBuildStatus: liveBuildStatus || 'failing',
|
|
2733
|
+
firstAt: sameHead ? (prior.firstAt || now) : now,
|
|
2734
|
+
lastAt: now,
|
|
2735
|
+
dispatchId: dispatchItem?.id || null,
|
|
2736
|
+
reason,
|
|
2737
|
+
};
|
|
2738
|
+
if (flippedToPaused) {
|
|
2739
|
+
try {
|
|
2740
|
+
const prNumber = target.prNumber || shared.getPrNumber(target) || target.id || 'unknown';
|
|
2741
|
+
const noteBody = `# Build fix ineffective — infra issue suspected: ${target.id || prNumber}\n\n`
|
|
2742
|
+
+ `**PR:** ${target.url || target.id || '(unknown)'}\n`
|
|
2743
|
+
+ `**Branch:** ${target.branch || '(unknown)'}\n`
|
|
2744
|
+
+ `**Head:** ${String(headSha || '').slice(0, 40) || '(unknown)'}\n`
|
|
2745
|
+
+ `**Attempt:** ${count}/${pauseAfter} (paused)\n\n`
|
|
2746
|
+
+ `A build-failure fix dispatch reported no branch change, but re-polling live CI showed the build is STILL \`${liveBuildStatus || 'failing'}\` against that exact commit. `
|
|
2747
|
+
+ `The engine reached \`engine.prNoOpFixPauseAttempts\` (${pauseAfter}) consecutive ineffective-fix attempts against the same head and has paused build-failure automation for this PR.\n\n`
|
|
2748
|
+
+ `**Infra issue suspected:** ${count} fix dispatches in a row landed on this head without moving it, and CI is still red. This pattern usually means the failure isn't fixable by another automated code-fix pass (flaky infra, an external outage, a broken pipeline/runner, a protected-branch or permissions issue, etc.) rather than a code bug — investigate the CI environment before re-dispatching.\n\n`
|
|
2749
|
+
+ `**Recovery options:**\n`
|
|
2750
|
+
+ `1. A new head SHA on this PR auto-clears the pause (a real fix attempt landed).\n`
|
|
2751
|
+
+ `2. Manually inspect the live CI logs — the automated fixes may be addressing the wrong failure, or the failure may not be fixable by re-pushing (flaky infra, protected branch, etc).\n`;
|
|
2752
|
+
shared.writeToInbox(
|
|
2753
|
+
'engine',
|
|
2754
|
+
`build-fix-ineffective-${prNumber}`,
|
|
2755
|
+
noteBody,
|
|
2756
|
+
null,
|
|
2757
|
+
{ wi: dispatchItem?.meta?.item?.id || null, pr: target.id || null, cause: shared.PR_FIX_CAUSE.BUILD_FAILURE }
|
|
2758
|
+
);
|
|
2759
|
+
} catch (err) {
|
|
2760
|
+
log('warn', `build-fix-ineffective inbox alert for ${target.id || '(unknown)'}: ${err.message}`);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return target._buildFixIneffective;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2650
2766
|
function recordPrNoOpFixAttempt(target, cause, source, dispatchItem, branchChange, config, noopReason) {
|
|
2651
2767
|
const evidenceFingerprint = shared.prFixEvidenceFingerprint(target, cause);
|
|
2652
2768
|
const prior = shared.getPrNoOpFixRecord(target, cause);
|
|
@@ -2992,6 +3108,22 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
2992
3108
|
return prs;
|
|
2993
3109
|
}
|
|
2994
3110
|
if (explicitlyChangedBranch && options.branchChange?.changed === false) {
|
|
3111
|
+
// #639 — a BUILD_FAILURE fix that reported "no branch change" is only a
|
|
3112
|
+
// genuine no-op if the build the fix was dispatched against actually
|
|
3113
|
+
// recovered. `options.buildStillFailing` (set by the caller from a live
|
|
3114
|
+
// re-poll of the current head commit) proves it did NOT — the same
|
|
3115
|
+
// failure persists against the exact commit the agent left in place.
|
|
3116
|
+
// Route through the distinct fixIneffective counter instead of the
|
|
3117
|
+
// shared no-op/pause counter so a stuck-red build gets its own signal
|
|
3118
|
+
// and its own (non-sticky-forever) pause, rather than silently
|
|
3119
|
+
// consuming the generic no-op pause budget with no way to tell "there
|
|
3120
|
+
// was truly nothing to do" apart from "the fix didn't work".
|
|
3121
|
+
if (cause === shared.PR_FIX_CAUSE.BUILD_FAILURE && options.buildStillFailing) {
|
|
3122
|
+
const record = recordBuildFixIneffective(target, options.dispatchItem, options.config, options.buildStillFailing);
|
|
3123
|
+
result = { noOp: true, cause, fixIneffective: true, paused: !!record.paused, count: record.count };
|
|
3124
|
+
log('warn', `Updated ${pr.id} → build-fix reported no branch change, but live CI is still ${options.buildStillFailing} on the same head; recorded fixIneffective attempt ${record.count}${record.paused ? ' (paused — infra issue suspected)' : ''}`);
|
|
3125
|
+
return prs;
|
|
3126
|
+
}
|
|
2995
3127
|
const record = recordPrNoOpFixAttempt(target, cause, source, options.dispatchItem, options.branchChange, options.config, options.noopReason);
|
|
2996
3128
|
result = { noOp: true, cause, paused: !!record.paused, count: record.count };
|
|
2997
3129
|
log('warn', `Updated ${pr.id} → recorded no-op ${cause} fix attempt ${record.count}${record.paused ? ' (paused)' : ''}; PR branch was unchanged`);
|
|
@@ -3022,6 +3154,10 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3022
3154
|
target._buildFixPushedAt = ts();
|
|
3023
3155
|
delete target._buildFixPushFailedCount;
|
|
3024
3156
|
delete target._buildFixNeedsHumanRebase;
|
|
3157
|
+
// #639 — a genuine branch-changing fix landed; drop any stale
|
|
3158
|
+
// fixIneffective pause from a prior head so the next ineffective
|
|
3159
|
+
// detection (if any) starts a fresh count against the new commit.
|
|
3160
|
+
delete target._buildFixIneffective;
|
|
3025
3161
|
}
|
|
3026
3162
|
if (source === 'pr-human-feedback') {
|
|
3027
3163
|
const clearPendingFix = shouldClearHumanFeedbackPendingFix(target, pr, automationCauseKey);
|
|
@@ -5729,6 +5865,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5729
5865
|
// Archive is manual — user archives plans from the dashboard when ready
|
|
5730
5866
|
|
|
5731
5867
|
let prFixBranchChange = null;
|
|
5868
|
+
let prFixBuildStillFailing = null;
|
|
5732
5869
|
if (type === WORK_TYPE.FIX && effectiveSuccess && meta?.pr?.id) {
|
|
5733
5870
|
try {
|
|
5734
5871
|
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
@@ -5736,6 +5873,30 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5736
5873
|
log('warn', `PR fix no-op detection for ${meta.pr.id}: ${err.message}`);
|
|
5737
5874
|
prFixBranchChange = { changed: null, reason: err.message };
|
|
5738
5875
|
}
|
|
5876
|
+
// #639 — a BUILD_FAILURE fix that didn't change the branch is not
|
|
5877
|
+
// necessarily a genuine no-op: the agent may have found its own (or a
|
|
5878
|
+
// prior dispatch's) fix commit already present and concluded "nothing to
|
|
5879
|
+
// change" without checking whether CI actually turned green on that
|
|
5880
|
+
// commit. Re-poll live CI for the head before letting updatePrAfterFix
|
|
5881
|
+
// accept the no-op — if it's still failing, flag it so the no-op path
|
|
5882
|
+
// records a distinct fixIneffective signal instead of a clean no-op.
|
|
5883
|
+
if (prFixBranchChange?.changed === false) {
|
|
5884
|
+
const fixCause = shared.getPrFixAutomationCause({
|
|
5885
|
+
dispatchKey: dispatchItem?.meta?.dispatchKey,
|
|
5886
|
+
source: meta?.source,
|
|
5887
|
+
task: dispatchItem?.task,
|
|
5888
|
+
});
|
|
5889
|
+
if (fixCause === shared.PR_FIX_CAUSE.BUILD_FAILURE) {
|
|
5890
|
+
try {
|
|
5891
|
+
const liveStatus = await checkBuildStillFailingLive(meta.pr, meta.project);
|
|
5892
|
+
if (liveStatus === shared.BUILD_STATUS.FAILING) {
|
|
5893
|
+
prFixBuildStillFailing = liveStatus;
|
|
5894
|
+
}
|
|
5895
|
+
} catch (err) {
|
|
5896
|
+
log('warn', `Live build re-check for ${meta.pr.id}: ${err.message}`);
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5739
5900
|
}
|
|
5740
5901
|
|
|
5741
5902
|
// Scheduled task back-reference: update schedule-runs.json and write linked inbox note
|
|
@@ -5854,6 +6015,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5854
6015
|
automationCauseKey: meta?.automationCauseKey,
|
|
5855
6016
|
dispatchItem,
|
|
5856
6017
|
branchChange: prFixBranchChange,
|
|
6018
|
+
buildStillFailing: prFixBuildStillFailing,
|
|
5857
6019
|
config,
|
|
5858
6020
|
noopReason: noopRationale || meta?._noopReason || '',
|
|
5859
6021
|
});
|
|
@@ -6666,6 +6828,11 @@ module.exports = {
|
|
|
6666
6828
|
// Issue #2969 — exported for direct unit testing of the pause-flip alert
|
|
6667
6829
|
// path without going through updatePrAfterFix's many guards.
|
|
6668
6830
|
recordPrNoOpFixAttempt,
|
|
6831
|
+
// #639 — exported for direct unit testing of the build-fix-ineffective
|
|
6832
|
+
// detection path (no-op branch + still-failing live CI) without spinning
|
|
6833
|
+
// up the full ADO/GH poller stack.
|
|
6834
|
+
checkBuildStillFailingLive,
|
|
6835
|
+
recordBuildFixIneffective,
|
|
6669
6836
|
// W-mq5uzmc6001d708f — post-hoc PR enrollment for orphan `_pr` pointers.
|
|
6670
6837
|
enrollPrFromCanonicalId,
|
|
6671
6838
|
_setEnrollmentGhRunnerForTest,
|
package/engine/queries.js
CHANGED
|
@@ -11,7 +11,7 @@ const os = require('os');
|
|
|
11
11
|
const shared = require('./shared');
|
|
12
12
|
const steering = require('./steering');
|
|
13
13
|
|
|
14
|
-
const { safeRead, safeReadDir, safeJson, safeWrite, getProjects, mutateJsonFileLocked,
|
|
14
|
+
const { safeRead, safeReadDir, safeJson, safeJsonObj, safeWrite, getProjects, mutateJsonFileLocked,
|
|
15
15
|
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
|
|
16
16
|
WI_STATUS, DONE_STATUSES, WORK_TYPE, PRD_ITEM_STATUS, PR_STATUS, ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS } = shared;
|
|
17
17
|
|
|
@@ -221,7 +221,7 @@ function migrateDeprecatedConfigPollKeysOnce() {
|
|
|
221
221
|
|
|
222
222
|
function getConfig() {
|
|
223
223
|
migrateDeprecatedConfigPollKeysOnce();
|
|
224
|
-
return
|
|
224
|
+
return safeJsonObj(CONFIG_PATH);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
function getControl() {
|
package/engine/shared.js
CHANGED
|
@@ -8671,6 +8671,24 @@ function getPrPausedCauses(pr) {
|
|
|
8671
8671
|
return causes;
|
|
8672
8672
|
}
|
|
8673
8673
|
|
|
8674
|
+
// #639 — Distinct pause signal for "fix reported no branch change, but a live
|
|
8675
|
+
// re-poll proved the build is STILL failing on that exact (unchanged) head".
|
|
8676
|
+
// This is deliberately tracked separately from `_noOpFixes[BUILD_FAILURE]`:
|
|
8677
|
+
// that record's evidence fingerprint includes `headRefOid`, so when the head
|
|
8678
|
+
// never moves (the agent genuinely pushed nothing) the fingerprint never
|
|
8679
|
+
// changes either — a real "build never actually got fixed" case would get
|
|
8680
|
+
// stuck paused forever with no distinguishing signal from a benign "nothing
|
|
8681
|
+
// needed doing" no-op (see #639). `_buildFixIneffective` re-validates against
|
|
8682
|
+
// the live head SHA the same way `isPrNoOpFixCausePaused` re-validates the
|
|
8683
|
+
// fingerprint: a new head SHA (real fix attempt landed) always releases the
|
|
8684
|
+
// pause, even though the underlying counter never got a fresh evidence key.
|
|
8685
|
+
function isBuildFixIneffectivePaused(pr) {
|
|
8686
|
+
const record = pr?._buildFixIneffective;
|
|
8687
|
+
if (!record || typeof record !== 'object' || record.paused !== true) return false;
|
|
8688
|
+
const currentHead = _prHeadSha(pr);
|
|
8689
|
+
return !!currentHead && record.headSha === currentHead;
|
|
8690
|
+
}
|
|
8691
|
+
|
|
8674
8692
|
/**
|
|
8675
8693
|
* Recursively purge Windows reserved-name pseudo-files (NUL, CON, PRN, AUX, etc.)
|
|
8676
8694
|
* using the \\?\ extended path prefix that bypasses reserved-name interpretation.
|
|
@@ -8758,11 +8776,60 @@ function isWorktreePathLive(worktreePath, opts = {}) {
|
|
|
8758
8776
|
return false;
|
|
8759
8777
|
}
|
|
8760
8778
|
|
|
8779
|
+
// P-1d6b1aa7 — separate, best-effort lookup of the specific dispatch row
|
|
8780
|
+
// that is blocking a wipe, so callers can fold self-diagnosing detail (id,
|
|
8781
|
+
// status, age-in-non-terminal-state) into the skip-live inbox note instead
|
|
8782
|
+
// of forcing a human to run a follow-up SQL query against
|
|
8783
|
+
// engine/state.db every time W-mq5rwwss000f30a7 recurs. Deliberately kept
|
|
8784
|
+
// separate from isWorktreePathLive so that helper's fail-open boolean
|
|
8785
|
+
// contract (and its existing callers/tests) are untouched — this is an
|
|
8786
|
+
// additional, purely-diagnostic query run only after the guard has already
|
|
8787
|
+
// fired. Any failure here (SQL unavailable, no matching row, etc.) resolves
|
|
8788
|
+
// to `null` and the note falls back to an "unavailable" label; it must
|
|
8789
|
+
// never throw or change wipe/skip behavior.
|
|
8790
|
+
function getWorktreeBlockingDispatchInfo(worktreePath, opts = {}) {
|
|
8791
|
+
try {
|
|
8792
|
+
const target = _normalizeWorktreePath(worktreePath);
|
|
8793
|
+
if (!target) return null;
|
|
8794
|
+
const excludeDispatchId = opts.excludeDispatchId ? String(opts.excludeDispatchId) : null;
|
|
8795
|
+
let db = opts.db || null;
|
|
8796
|
+
if (!db) {
|
|
8797
|
+
try { db = require('./db').getDb(); }
|
|
8798
|
+
catch { return null; }
|
|
8799
|
+
}
|
|
8800
|
+
if (!db) return null;
|
|
8801
|
+
const rows = db.prepare(`
|
|
8802
|
+
SELECT id, status, updated_at,
|
|
8803
|
+
json_extract(data, '$.worktreePath') AS top_wt,
|
|
8804
|
+
json_extract(data, '$.meta.worktreePath') AS meta_wt
|
|
8805
|
+
FROM dispatches
|
|
8806
|
+
WHERE status IN ('pending', 'active')
|
|
8807
|
+
`).all();
|
|
8808
|
+
for (const row of rows || []) {
|
|
8809
|
+
if (excludeDispatchId && String(row.id) === excludeDispatchId) continue;
|
|
8810
|
+
const topMatch = row.top_wt && _normalizeWorktreePath(row.top_wt) === target;
|
|
8811
|
+
const metaMatch = row.meta_wt && _normalizeWorktreePath(row.meta_wt) === target;
|
|
8812
|
+
if (topMatch || metaMatch) {
|
|
8813
|
+
const updatedAt = Number(row.updated_at) || 0;
|
|
8814
|
+
const ageMs = updatedAt > 0 ? Math.max(0, Date.now() - updatedAt) : null;
|
|
8815
|
+
return { id: row.id, status: row.status, ageMs };
|
|
8816
|
+
}
|
|
8817
|
+
}
|
|
8818
|
+
return null;
|
|
8819
|
+
} catch {
|
|
8820
|
+
return null; // best-effort diagnostics only — never throw
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8823
|
+
|
|
8761
8824
|
// Drop a deduped inbox note when a wipe site skips due to the live guard so
|
|
8762
8825
|
// operators can see when the guard fires. Filename is keyed on basename +
|
|
8763
8826
|
// UTC date — a single skip per worktree per day produces one note; further
|
|
8764
|
-
// skips that day silently no-op.
|
|
8765
|
-
|
|
8827
|
+
// skips that day silently no-op. `blockingInfo` (optional, shape
|
|
8828
|
+
// `{ id, status, ageMs }` from getWorktreeBlockingDispatchInfo) folds the
|
|
8829
|
+
// blocking dispatch's diagnostics into the note body so it is
|
|
8830
|
+
// self-diagnosing; when unavailable (no matching row, SQL unreachable,
|
|
8831
|
+
// etc.) the note says so explicitly instead of requiring a follow-up query.
|
|
8832
|
+
function _writeWorktreeSkipLiveInboxNote(worktreePath, callerTag, blockingInfo) {
|
|
8766
8833
|
try {
|
|
8767
8834
|
const base = path.basename(String(worktreePath || '').replace(/[\\/]+$/g, '')) || 'unknown';
|
|
8768
8835
|
const safeBase = base.replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 80);
|
|
@@ -8772,6 +8839,13 @@ function _writeWorktreeSkipLiveInboxNote(worktreePath, callerTag) {
|
|
|
8772
8839
|
try { fs.mkdirSync(inboxDir, { recursive: true }); } catch { /* exists */ }
|
|
8773
8840
|
const fpath = path.join(inboxDir, fname);
|
|
8774
8841
|
if (fs.existsSync(fpath)) return; // deduped
|
|
8842
|
+
const diagnosticsLines = (blockingInfo && blockingInfo.id)
|
|
8843
|
+
? [
|
|
8844
|
+
`- blocking dispatch id: ${blockingInfo.id}`,
|
|
8845
|
+
`- blocking dispatch status: ${blockingInfo.status || 'unknown'}`,
|
|
8846
|
+
`- blocking dispatch age: ${blockingInfo.ageMs != null ? `${Math.round(blockingInfo.ageMs / 1000)}s since last update` : 'unavailable'}`,
|
|
8847
|
+
]
|
|
8848
|
+
: ['- blocking dispatch diagnostics: unavailable (no matching row found, or SQL was unreachable)'];
|
|
8775
8849
|
const body = [
|
|
8776
8850
|
'---',
|
|
8777
8851
|
`id: NOTE-${crypto.randomBytes(8).toString('hex')}`,
|
|
@@ -8784,6 +8858,7 @@ function _writeWorktreeSkipLiveInboxNote(worktreePath, callerTag) {
|
|
|
8784
8858
|
`- caller: ${callerTag || 'unknown'}`,
|
|
8785
8859
|
`- worktree: ${worktreePath}`,
|
|
8786
8860
|
`- timestamp: ${new Date().toISOString()}`,
|
|
8861
|
+
...diagnosticsLines,
|
|
8787
8862
|
'',
|
|
8788
8863
|
'A non-terminal dispatch row still claims this worktree. The wipe was skipped to',
|
|
8789
8864
|
'protect agent state. If this fires repeatedly, inspect engine/state.db dispatches',
|
|
@@ -8863,7 +8938,8 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
|
8863
8938
|
const excludeDispatchId = opts && opts.excludeDispatchId ? String(opts.excludeDispatchId) : null;
|
|
8864
8939
|
if (isWorktreePathLive(resolved, excludeDispatchId ? { excludeDispatchId } : undefined)) {
|
|
8865
8940
|
log('warn', `removeWorktree: skip — live dispatch in ${wtPath}`);
|
|
8866
|
-
|
|
8941
|
+
const blockingInfo = getWorktreeBlockingDispatchInfo(resolved, excludeDispatchId ? { excludeDispatchId } : undefined);
|
|
8942
|
+
_writeWorktreeSkipLiveInboxNote(wtPath, 'shared.removeWorktree', blockingInfo);
|
|
8867
8943
|
return false;
|
|
8868
8944
|
}
|
|
8869
8945
|
_pruneRemoveWorktreeFailures();
|
|
@@ -9460,6 +9536,7 @@ module.exports = {
|
|
|
9460
9536
|
getPrNoOpFixRecord,
|
|
9461
9537
|
isPrNoOpFixCausePaused,
|
|
9462
9538
|
getPrPausedCauses,
|
|
9539
|
+
isBuildFixIneffectivePaused,
|
|
9463
9540
|
parseSkillFrontmatter,
|
|
9464
9541
|
sleepMs,
|
|
9465
9542
|
killGracefully,
|
|
@@ -9477,6 +9554,7 @@ module.exports = {
|
|
|
9477
9554
|
listProcessReachable,
|
|
9478
9555
|
removeWorktree,
|
|
9479
9556
|
isWorktreePathLive,
|
|
9557
|
+
getWorktreeBlockingDispatchInfo,
|
|
9480
9558
|
WORKTREE_OWNER_MARKER,
|
|
9481
9559
|
writeWorktreeOwnerMarker,
|
|
9482
9560
|
hasWorktreeOwnerMarker,
|
package/engine.js
CHANGED
|
@@ -1697,7 +1697,8 @@ async function _quarantineDirtyWorktree(rootDir, worktreePath, branchName, gitOp
|
|
|
1697
1697
|
// agent's git child processes either.
|
|
1698
1698
|
if (shared.isWorktreePathLive(worktreePath)) {
|
|
1699
1699
|
log('warn', `_quarantineDirtyWorktree: skip — live dispatch in ${worktreePath}`);
|
|
1700
|
-
shared.
|
|
1700
|
+
const blockingInfo = shared.getWorktreeBlockingDispatchInfo(worktreePath);
|
|
1701
|
+
shared._writeWorktreeSkipLiveInboxNote(worktreePath, '_quarantineDirtyWorktree', blockingInfo);
|
|
1701
1702
|
return { quarantinedPath: null, backupRef: null, skipped: true };
|
|
1702
1703
|
}
|
|
1703
1704
|
|
|
@@ -6619,6 +6620,18 @@ function isPrNoOpFixCauseSuppressed(pr, cause) {
|
|
|
6619
6620
|
return true;
|
|
6620
6621
|
}
|
|
6621
6622
|
|
|
6623
|
+
// #639 — distinct suppression check for the build-fix-ineffective pause
|
|
6624
|
+
// (see shared.isBuildFixIneffectivePaused / engine/lifecycle.js
|
|
6625
|
+
// recordBuildFixIneffective). Kept separate from isPrNoOpFixCauseSuppressed
|
|
6626
|
+
// so the log line is unambiguous about WHY build-fix automation stopped:
|
|
6627
|
+
// repeated attempts genuinely found nothing to change vs. repeated attempts
|
|
6628
|
+
// left the build still red on the same commit.
|
|
6629
|
+
function isBuildFixIneffectiveSuppressed(pr) {
|
|
6630
|
+
if (!shared.isBuildFixIneffectivePaused(pr)) return false;
|
|
6631
|
+
log('warn', `PR ${pr.id}: suppressing build-failure automation — live CI stayed failing across repeated fix attempts on the same head; waiting for a new commit or human resume`);
|
|
6632
|
+
return true;
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6622
6635
|
const PR_PENDING_MISSING_BRANCH = shared.PR_PENDING_REASON.MISSING_BRANCH;
|
|
6623
6636
|
|
|
6624
6637
|
function normalizePrBranch(value) {
|
|
@@ -7330,7 +7343,8 @@ async function discoverFromPrs(config, project) {
|
|
|
7330
7343
|
}
|
|
7331
7344
|
if (pollEnabled && autoFixBuilds && pr.status === PR_STATUS.ACTIVE && pr.buildStatus === 'failing'
|
|
7332
7345
|
&& !fixDispatched
|
|
7333
|
-
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.BUILD_FAILURE)
|
|
7346
|
+
&& !isPrNoOpFixCauseSuppressed(pr, shared.PR_FIX_CAUSE.BUILD_FAILURE)
|
|
7347
|
+
&& !isBuildFixIneffectiveSuppressed(pr)) {
|
|
7334
7348
|
// W-mpritzcr0004afc5 (#2955): "don't fan out a parallel build-failure
|
|
7335
7349
|
// fix while the review-feedback path owns the PR" — implemented via
|
|
7336
7350
|
// the `!fixDispatched` gate above (mirrors the merge-conflict block
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2314",
|
|
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"
|