@yemi33/minions 0.1.2322 → 0.1.2323
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/render-other.js +17 -1
- package/dashboard/js/render-prs.js +50 -8
- package/dashboard/slim/body.html +6 -1
- package/dashboard/slim/js/modals-tiles.js +23 -116
- package/dashboard/slim/styles.css +10 -27
- package/dashboard.js +58 -0
- package/engine/consolidation.js +69 -7
- package/engine/lifecycle.js +15 -0
- package/engine/live-checkout.js +21 -0
- package/engine/shared.js +41 -1
- package/engine.js +1 -1
- package/package.json +1 -1
|
@@ -69,8 +69,24 @@ function _renderProjectBranch(p) {
|
|
|
69
69
|
? ' <span class="muted">→ ' + mainBranch + '</span>'
|
|
70
70
|
: '';
|
|
71
71
|
const detachedSuffix = p.gitDetached ? ' <span class="muted">(detached)</span>' : '';
|
|
72
|
+
// opg-microsoft/minions#664 — the counts are always vs origin/<comparator>
|
|
73
|
+
// (configured mainBranch, falling back to remoteDefaultBranch; see
|
|
74
|
+
// engine/queries.js _probeProjectGitStatus), never vs the checked-out
|
|
75
|
+
// branch's own upstream. When branchDiffersFromMain the bare "↑N ↓M" chip
|
|
76
|
+
// reads as ambiguous (looks like it could be vs the current branch's
|
|
77
|
+
// tracking ref), so append the comparator name to the VISIBLE text too —
|
|
78
|
+
// not just the hover title — so the disambiguation doesn't require a
|
|
79
|
+
// hover. Skip the suffix when the checked-out branch IS the comparator
|
|
80
|
+
// since there's nothing to disambiguate.
|
|
81
|
+
const comparatorBranch = mainBranch || remoteDefault;
|
|
82
|
+
// Deliberately separate from branchDiffersFromMain (which only fires when a
|
|
83
|
+
// configured mainBranch is set, driving the "→ main" suffix): the ahead/
|
|
84
|
+
// behind comparator falls back to remoteDefaultBranch too, so the chip must
|
|
85
|
+
// disambiguate against whichever one actually produced the counts.
|
|
86
|
+
const branchDiffersFromComparator = !!(comparatorBranch && p.gitBranch !== (p.mainBranch || p.remoteDefaultBranch));
|
|
87
|
+
const comparatorSuffix = branchDiffersFromComparator ? ' vs ' + comparatorBranch : '';
|
|
72
88
|
const aheadBehind = (ahead !== null && behind !== null && (ahead > 0 || behind > 0))
|
|
73
|
-
? ' <span class="project-ab" title="' + escHtml('Ahead ' + ahead + ' / behind ' + behind + ' vs origin/' + (p.mainBranch || p.remoteDefaultBranch)) + '">↑' + ahead + ' ↓' + behind + '</span>'
|
|
89
|
+
? ' <span class="project-ab" title="' + escHtml('Ahead ' + ahead + ' / behind ' + behind + ' vs origin/' + (p.mainBranch || p.remoteDefaultBranch)) + '">↑' + ahead + ' ↓' + behind + comparatorSuffix + '</span>'
|
|
74
90
|
: '';
|
|
75
91
|
// Warning chip surfaces when the configured main disagrees with the remote
|
|
76
92
|
// default branch (the #2732 case: config=master, origin/HEAD=main) OR when
|
|
@@ -202,8 +202,13 @@ function prRow(pr) {
|
|
|
202
202
|
// clearly-labeled indicator — a still-red build after an unchanged fix
|
|
203
203
|
// attempt usually means the failure isn't fixable by another agent pass
|
|
204
204
|
// (flaky infra, external outage, etc.), hence "infra issue suspected".
|
|
205
|
-
//
|
|
206
|
-
//
|
|
205
|
+
// Issue #671 — clicking resumes it via /api/pull-requests/clear-build-fix-ineffective
|
|
206
|
+
// (resumeBuildFixIneffective below), which clears _buildFixIneffective so
|
|
207
|
+
// BUILD_FAILURE auto-fix dispatch can resume. A new push already resets
|
|
208
|
+
// this pause automatically (see shared.isBuildFixIneffectivePaused); the
|
|
209
|
+
// click handler exists for the case where an operator has already
|
|
210
|
+
// confirmed (e.g. a fresh CI run queued against a rebased/retried head)
|
|
211
|
+
// and doesn't want to wait for the next poll to notice.
|
|
207
212
|
var buildFixIneffective = pr._buildFixIneffective;
|
|
208
213
|
if (buildFixIneffective && typeof buildFixIneffective === 'object' && buildFixIneffective.paused === true) {
|
|
209
214
|
var bfiTitle = 'Build-fix reported no branch change, and a live CI re-poll confirmed the build is still "'
|
|
@@ -211,11 +216,14 @@ function prRow(pr) {
|
|
|
211
216
|
+ (buildFixIneffective.headSha || 'unknown head') + ') after '
|
|
212
217
|
+ (buildFixIneffective.count || '?') + ' ineffective attempt(s). Reason: '
|
|
213
218
|
+ (buildFixIneffective.reason || 'unknown')
|
|
214
|
-
+ '. A new push to this branch will reset this pause.';
|
|
215
|
-
pausedChips += ' <
|
|
216
|
-
+ 'style="font-size:var(--text-xs)" '
|
|
217
|
-
+ 'title="' + escapeHtml(bfiTitle) + '"
|
|
218
|
-
+ '
|
|
219
|
+
+ '. A new push to this branch will reset this pause automatically, or click to resume now.';
|
|
220
|
+
pausedChips += ' <button class="pr-badge rejected pr-build-fix-ineffective-chip" '
|
|
221
|
+
+ 'style="font-size:var(--text-xs);cursor:pointer;border:none" '
|
|
222
|
+
+ 'title="' + escapeHtml(bfiTitle) + '" '
|
|
223
|
+
+ 'data-pr-id="' + escapeHtml(String(pr.id || '')) + '" '
|
|
224
|
+
+ 'data-pr-head-sha="' + escapeHtml(String(buildFixIneffective.headSha || '')) + '" '
|
|
225
|
+
+ 'onclick="event.stopPropagation();resumeBuildFixIneffective(this)">'
|
|
226
|
+
+ '⏸ infra issue suspected ▶</button>';
|
|
219
227
|
}
|
|
220
228
|
const titleText = pr.title || 'Untitled';
|
|
221
229
|
// Polish (W-mqv5ccn7): flatten Markdown in the body so the preview shows
|
|
@@ -749,4 +757,38 @@ async function resumePausedCause(btn) {
|
|
|
749
757
|
}
|
|
750
758
|
}
|
|
751
759
|
|
|
752
|
-
|
|
760
|
+
// Issue #671 — resume a paused build-fix-ineffective ("infra issue
|
|
761
|
+
// suspected") pause. Mirrors resumePausedCause above but targets the
|
|
762
|
+
// headSha-keyed _buildFixIneffective record via its own endpoint. Sends the
|
|
763
|
+
// headSha the chip was rendered with so the server can reject the clear as
|
|
764
|
+
// stale if the pause has since moved to a different commit.
|
|
765
|
+
async function resumeBuildFixIneffective(btn) {
|
|
766
|
+
if (!btn || btn.disabled) return;
|
|
767
|
+
const prId = btn.dataset.prId;
|
|
768
|
+
const headSha = btn.dataset.prHeadSha || '';
|
|
769
|
+
if (!prId) return;
|
|
770
|
+
if (!confirm('Resume build-fix automation on ' + prId + '?\n\nThis clears the "infra issue suspected" pause so the engine can re-dispatch build fixes on the next discovery pass.')) return;
|
|
771
|
+
btn.disabled = true;
|
|
772
|
+
const prevDisplay = btn.style.display;
|
|
773
|
+
btn.style.display = 'none';
|
|
774
|
+
showToast('pr-toast', 'Resumed build-fix automation on ' + prId, true);
|
|
775
|
+
try {
|
|
776
|
+
const res = await fetch('/api/pull-requests/clear-build-fix-ineffective', {
|
|
777
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
778
|
+
body: JSON.stringify({ prId, headSha })
|
|
779
|
+
});
|
|
780
|
+
if (!res.ok) {
|
|
781
|
+
const d = await res.json().catch(() => ({}));
|
|
782
|
+
btn.style.display = prevDisplay;
|
|
783
|
+
btn.disabled = false;
|
|
784
|
+
showToast('pr-toast', 'Failed: ' + (d.error || 'unknown'), false);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
} catch (e) {
|
|
788
|
+
btn.style.display = prevDisplay;
|
|
789
|
+
btn.disabled = false;
|
|
790
|
+
showToast('pr-toast', 'Error: ' + e.message, false);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, openPrDetail, unlinkPr, togglePrObserve, resumePausedCause, resumeBuildFixIneffective };
|
package/dashboard/slim/body.html
CHANGED
|
@@ -231,7 +231,12 @@
|
|
|
231
231
|
the standard TILE_VIEWS path in modals-tiles.js#renderPlansBody. There is no
|
|
232
232
|
slim-specific Plans/PRD flyout markup anymore (W-mr28ko750010199f / "reuse,
|
|
233
233
|
don't fork"): the classic /plans screen is the single source of truth for the
|
|
234
|
-
Plans + PRD tabs and every lifecycle action.
|
|
234
|
+
Plans + PRD tabs and every lifecycle action.
|
|
235
|
+
The "Active PRs" cockpit tile follows the same iframe-embed pattern
|
|
236
|
+
(W-mr3l7y0m0007102a): it opens the LITERAL classic /prs screen in a chrome-off
|
|
237
|
+
iframe (/prs?embed=1) via modals-tiles.js#renderPrsBody, so the full PR
|
|
238
|
+
filter/sort UI + row actions (comment/track/link/pr-action) are the classic
|
|
239
|
+
screen — no slim-specific PR list to maintain. -->
|
|
235
240
|
|
|
236
241
|
<!-- Pin editor — create (Pin Content action / "+ Pin") or edit an existing
|
|
237
242
|
note. Submits to POST /api/pinned (create) or /api/pinned/update (edit). -->
|
|
@@ -81,17 +81,6 @@
|
|
|
81
81
|
return card;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// Parse the org/project slug from a canonical PR id, dropping the host prefix
|
|
85
|
-
// and the trailing "#<num>". E.g. "ado:office/iss/constellation#5315603" →
|
|
86
|
-
// "office/iss/constellation", "github:yemi33/minions#2911" → "yemi33/minions".
|
|
87
|
-
function parsePrSlug(id) {
|
|
88
|
-
if (!id || typeof id !== 'string') return '';
|
|
89
|
-
var hash = id.indexOf('#');
|
|
90
|
-
var head = hash >= 0 ? id.slice(0, hash) : id;
|
|
91
|
-
var colon = head.indexOf(':');
|
|
92
|
-
return colon >= 0 ? head.slice(colon + 1) : head;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
84
|
// Per-tile body renderers for the detail modal. Each reads the latest status
|
|
96
85
|
// snapshot and fills the modal body; openTileModal looks them up by key.
|
|
97
86
|
function renderEngineTileBody(body, data) {
|
|
@@ -160,107 +149,23 @@
|
|
|
160
149
|
body.appendChild(frame);
|
|
161
150
|
}
|
|
162
151
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
};
|
|
181
|
-
var REVIEW_STATUS = {
|
|
182
|
-
approved: { glyph: '✅', label: 'approved' },
|
|
183
|
-
'changes-requested': { glyph: '❌', label: 'changes requested' },
|
|
184
|
-
changes_requested: { glyph: '❌', label: 'changes requested' },
|
|
185
|
-
'requested-changes': { glyph: '❌', label: 'changes requested' },
|
|
186
|
-
rejected: { glyph: '❌', label: 'rejected' },
|
|
187
|
-
pending: { glyph: '💬', label: 'pending' },
|
|
188
|
-
commented: { glyph: '💬', label: 'commented' },
|
|
189
|
-
};
|
|
190
|
-
function statusSpan(subsystem, raw, table) {
|
|
191
|
-
var info = table[String(raw || '').toLowerCase()];
|
|
192
|
-
var glyph = info ? info.glyph : '➖';
|
|
193
|
-
var label = info ? info.label : (raw ? String(raw) : 'none');
|
|
194
|
-
var span = document.createElement('span');
|
|
195
|
-
span.className = 'pr-status';
|
|
196
|
-
span.textContent = glyph + ' ' + subsystem + ' ' + label;
|
|
197
|
-
span.title = subsystem + ': ' + (raw || 'unknown');
|
|
198
|
-
return span;
|
|
199
|
-
}
|
|
200
|
-
var prs = Array.isArray(data.pullRequests) ? data.pullRequests : [];
|
|
201
|
-
if (!prs.length) { tileEmpty(body, 'No tracked pull requests.'); return; }
|
|
202
|
-
function prCard(p) {
|
|
203
|
-
var num = p.prNumber || p.number || p.id;
|
|
204
|
-
var build = p.buildStatus || '';
|
|
205
|
-
var review = p.reviewStatus || '';
|
|
206
|
-
var cls = p.status === 'merged' ? 'green'
|
|
207
|
-
: (build === 'failing' ? 'red'
|
|
208
|
-
: ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
|
|
209
|
-
var statusText = document.createElement('span');
|
|
210
|
-
statusText.textContent = p.status || 'unknown';
|
|
211
|
-
return tileCard({
|
|
212
|
-
title: (num ? '#' + num + ' ' : '') + (p.title || '(untitled PR)'),
|
|
213
|
-
// Project slug first (under title), then build/review row below — the
|
|
214
|
-
// project is the strongest disambiguator across PRs and benefits from
|
|
215
|
-
// being adjacent to the title.
|
|
216
|
-
subtle: parsePrSlug(p.id),
|
|
217
|
-
subtleBeforeMeta: true,
|
|
218
|
-
metaNodes: [statusText, statusSpan('build', build, BUILD_STATUS), statusSpan('review', review, REVIEW_STATUS)],
|
|
219
|
-
chip: { text: p.status || '—', cls: cls },
|
|
220
|
-
href: p.url || null,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
// Partition once into three fixed buckets (status normalized to lowercase),
|
|
224
|
-
// preserving each PR's relative order within its bucket. Mirrors shared
|
|
225
|
-
// PR_STATUS (active/merged/abandoned/closed/linked); anything that does not
|
|
226
|
-
// match the merged/abandoned buckets defaults to Active so a PR with an
|
|
227
|
-
// unexpected status never silently disappears.
|
|
228
|
-
var buckets = { active: [], merged: [], abandoned: [] };
|
|
229
|
-
prs.forEach(function(p) {
|
|
230
|
-
var s = String(p.status || '').toLowerCase();
|
|
231
|
-
if (s === 'abandoned') buckets.abandoned.push(p);
|
|
232
|
-
else if (s === 'merged' || s === 'completed' || s === 'closed') buckets.merged.push(p);
|
|
233
|
-
else buckets.active.push(p);
|
|
234
|
-
});
|
|
235
|
-
// Fixed section order: Active (open) → Merged / Completed → Abandoned.
|
|
236
|
-
// Empty sections are omitted entirely.
|
|
237
|
-
var sections = [
|
|
238
|
-
{ label: 'Active', prs: buckets.active, open: true },
|
|
239
|
-
{ label: 'Merged / Completed', prs: buckets.merged, open: false },
|
|
240
|
-
{ label: 'Abandoned', prs: buckets.abandoned, open: false },
|
|
241
|
-
];
|
|
242
|
-
sections.forEach(function(section) {
|
|
243
|
-
if (!section.prs.length) return;
|
|
244
|
-
var details = document.createElement('details');
|
|
245
|
-
details.className = 'tile-section';
|
|
246
|
-
if (section.open) details.open = true;
|
|
247
|
-
var summary = document.createElement('summary');
|
|
248
|
-
summary.className = 'tile-section-summary';
|
|
249
|
-
var label = document.createElement('span');
|
|
250
|
-
label.className = 'tile-section-label';
|
|
251
|
-
label.textContent = section.label;
|
|
252
|
-
var count = document.createElement('span');
|
|
253
|
-
count.className = 'tile-section-count';
|
|
254
|
-
count.textContent = '(' + section.prs.length + ')';
|
|
255
|
-
summary.appendChild(label);
|
|
256
|
-
summary.appendChild(count);
|
|
257
|
-
details.appendChild(summary);
|
|
258
|
-
var sectionBody = document.createElement('div');
|
|
259
|
-
sectionBody.className = 'tile-section-body';
|
|
260
|
-
section.prs.forEach(function(p) { sectionBody.appendChild(prCard(p)); });
|
|
261
|
-
details.appendChild(sectionBody);
|
|
262
|
-
body.appendChild(details);
|
|
263
|
-
});
|
|
152
|
+
// Pull-requests tile body — reuses the LITERAL classic dashboard /prs screen
|
|
153
|
+
// instead of a slim-specific bucketed PR list, so there is one PRs UI, not two
|
|
154
|
+
// (W-mr3l7y0m0007102a / "reuse, don't fork" — mirrors renderQueuedWorkBody for
|
|
155
|
+
// the Work tile and renderPlansBody for the Plans tile). Slim and classic are
|
|
156
|
+
// two separate IIFE bundles with no shared scope, so we embed the real /prs
|
|
157
|
+
// screen in an iframe with the chrome-off ?embed=1 mode. The iframed page IS
|
|
158
|
+
// the classic screen — the full PR list with every filter/sort + row action
|
|
159
|
+
// (comment/track/link/pr-action), backed by the same /api/status pullRequests
|
|
160
|
+
// data + endpoints — with zero duplicated rendering logic. Classic is reachable
|
|
161
|
+
// at /prs even with slim-ux ON (only / is taken over). Built with createElement
|
|
162
|
+
// (no innerHTML) to satisfy the dashboard no-unsanitized lint gate.
|
|
163
|
+
function renderPrsBody(body) {
|
|
164
|
+
var frame = document.createElement('iframe');
|
|
165
|
+
frame.className = 'slim-prs-embed';
|
|
166
|
+
frame.src = '/prs?embed=1';
|
|
167
|
+
frame.title = 'Pull Requests';
|
|
168
|
+
body.appendChild(frame);
|
|
264
169
|
}
|
|
265
170
|
|
|
266
171
|
function renderWatchTileBody(body, data) {
|
|
@@ -283,7 +188,7 @@
|
|
|
283
188
|
dispatches: { title: 'Active dispatches', render: function(body, data) { renderDispatchTileBody(body, data, true); } },
|
|
284
189
|
queued: { title: 'Queued work', render: function(body) { renderQueuedWorkBody(body); } },
|
|
285
190
|
plans: { title: 'Plans', render: function(body) { renderPlansBody(body); } },
|
|
286
|
-
prs: { title: 'Pull requests', render:
|
|
191
|
+
prs: { title: 'Pull requests', render: function(body) { renderPrsBody(body); } },
|
|
287
192
|
watches: { title: 'Watches', render: renderWatchTileBody },
|
|
288
193
|
};
|
|
289
194
|
|
|
@@ -304,11 +209,13 @@
|
|
|
304
209
|
// The "+ Link PR" header chip only applies to the PR list.
|
|
305
210
|
var headerChip = document.getElementById('slim-tile-modal-linkpr');
|
|
306
211
|
if (headerChip) headerChip.style.display = (key === 'prs') ? '' : 'none';
|
|
307
|
-
// The queued +
|
|
308
|
-
// iframe — widen the modal + drop the body padding so the
|
|
309
|
-
// gets real estate.
|
|
212
|
+
// The queued, plans + prs tiles embed a full classic screen (/work, /plans,
|
|
213
|
+
// /prs) in an iframe — widen the modal + drop the body padding so the
|
|
214
|
+
// embedded screen gets real estate. All three reuse the shared wide-modal
|
|
215
|
+
// treatment.
|
|
310
216
|
modal.classList.toggle('tile-modal--work', key === 'queued');
|
|
311
217
|
modal.classList.toggle('tile-modal--plans', key === 'plans');
|
|
218
|
+
modal.classList.toggle('tile-modal--prs', key === 'prs');
|
|
312
219
|
view.render(body, lastStatusData || {});
|
|
313
220
|
modal.classList.add('open');
|
|
314
221
|
}
|
|
@@ -680,15 +680,19 @@
|
|
|
680
680
|
/* Cockpit-tile detail modal: list of dispatches / PRs / watches, or a
|
|
681
681
|
key/value engine readout (reuses .agent-detail-row). */
|
|
682
682
|
#slim-tile-modal .modal { width: 720px; max-width: calc(100vw - 32px); }
|
|
683
|
-
/* W-mqrdggys000f94a4 / W-mr28ko750010199f
|
|
684
|
-
embed a full classic screen (/work,
|
|
685
|
-
|
|
683
|
+
/* W-mqrdggys000f94a4 / W-mr28ko750010199f / W-mr3l7y0m0007102a — the
|
|
684
|
+
Queued-work + Plans + PRs tiles embed a full classic screen (/work,
|
|
685
|
+
/plans, /prs) in an iframe; widen the modal + remove body padding so the
|
|
686
|
+
embedded screen gets the full width/height. */
|
|
686
687
|
#slim-tile-modal.tile-modal--work .modal,
|
|
687
|
-
#slim-tile-modal.tile-modal--plans .modal
|
|
688
|
+
#slim-tile-modal.tile-modal--plans .modal,
|
|
689
|
+
#slim-tile-modal.tile-modal--prs .modal { width: 1180px; }
|
|
688
690
|
#slim-tile-modal.tile-modal--work .modal-body,
|
|
689
|
-
#slim-tile-modal.tile-modal--plans .modal-body
|
|
691
|
+
#slim-tile-modal.tile-modal--plans .modal-body,
|
|
692
|
+
#slim-tile-modal.tile-modal--prs .modal-body { padding: 0; }
|
|
690
693
|
.slim-work-embed,
|
|
691
|
-
.slim-plans-embed
|
|
694
|
+
.slim-plans-embed,
|
|
695
|
+
.slim-prs-embed {
|
|
692
696
|
display: block; width: 100%; height: calc(100vh - 160px); min-height: 360px;
|
|
693
697
|
border: 0; background: var(--bg);
|
|
694
698
|
}
|
|
@@ -711,7 +715,6 @@
|
|
|
711
715
|
}
|
|
712
716
|
.tile-item-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; word-break: break-word; }
|
|
713
717
|
.tile-item-subtle { font-size: var(--text-sm); color: var(--muted); margin-top: 2px; word-break: break-word; }
|
|
714
|
-
.pr-status { display: inline-block; margin: 0 1px; text-decoration: none; }
|
|
715
718
|
.tile-chip {
|
|
716
719
|
flex: 0 0 auto;
|
|
717
720
|
font-size: var(--text-base); font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
|
|
@@ -724,26 +727,6 @@
|
|
|
724
727
|
.tile-chip.blue { background: rgba(88, 166, 255, 0.12); color: var(--blue); border-color: var(--blue); }
|
|
725
728
|
.tile-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); }
|
|
726
729
|
|
|
727
|
-
/* Collapsible PR sections (Active / Merged-Completed / Abandoned). Uses
|
|
728
|
-
native <details>/<summary> — no JS wiring needed for the toggle. */
|
|
729
|
-
.tile-section { margin-bottom: 10px; }
|
|
730
|
-
.tile-section:last-child { margin-bottom: 0; }
|
|
731
|
-
.tile-section-summary {
|
|
732
|
-
display: flex; align-items: center; gap: 6px;
|
|
733
|
-
cursor: pointer; user-select: none;
|
|
734
|
-
padding: 4px 2px; margin-bottom: 6px;
|
|
735
|
-
list-style: none;
|
|
736
|
-
font-size: var(--text-base); color: var(--muted);
|
|
737
|
-
}
|
|
738
|
-
.tile-section-summary::-webkit-details-marker { display: none; }
|
|
739
|
-
.tile-section-summary::before {
|
|
740
|
-
content: "▸"; color: var(--muted); font-size: var(--text-sm);
|
|
741
|
-
transition: transform 0.15s ease;
|
|
742
|
-
}
|
|
743
|
-
.tile-section[open] > .tile-section-summary::before { content: "▾"; }
|
|
744
|
-
.tile-section-label { font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; color: var(--text); }
|
|
745
|
-
.tile-section-count { color: var(--muted); }
|
|
746
|
-
|
|
747
730
|
/* Pinned-context list rows (Knowledge modal → Pinned Context tab). */
|
|
748
731
|
.pinned-row {
|
|
749
732
|
border: 1px solid var(--border);
|
package/dashboard.js
CHANGED
|
@@ -3959,6 +3959,7 @@ function _buildSyntheticActionResultsForTurn(turnId, message, requestedAt) {
|
|
|
3959
3959
|
if (entry.id) result.id = entry.id;
|
|
3960
3960
|
if (entry.project) result.project = entry.project;
|
|
3961
3961
|
if (entry.path) result.path = entry.path;
|
|
3962
|
+
if (entry.duplicate) result.duplicate = true;
|
|
3962
3963
|
if (Array.isArray(entry.followups) && entry.followups.length) result.followups = entry.followups;
|
|
3963
3964
|
results.push(result);
|
|
3964
3965
|
}
|
|
@@ -6914,6 +6915,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
6914
6915
|
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
6915
6916
|
if (!createResult.created) {
|
|
6916
6917
|
const duplicateId = createResult.duplicateOf || createResult.item?.id;
|
|
6918
|
+
// W-mr466ocx000l1191 — record the CC turn creation on the dedup path
|
|
6919
|
+
// too. The API call still succeeded (ok:true) from the caller's
|
|
6920
|
+
// perspective; without this, a CC turn that dispatches a work item
|
|
6921
|
+
// matching an existing one silently loses its confirmation chip
|
|
6922
|
+
// (_buildSyntheticActionResultsForTurn drains an empty list for the
|
|
6923
|
+
// turn, so donePayload.actions/actionResults come back empty even
|
|
6924
|
+
// though the LLM's tool call succeeded).
|
|
6925
|
+
recordCcTurnIfPresent(req, { kind: 'work-item', id: duplicateId, title: item.title, project: item.project || null, duplicate: true });
|
|
6917
6926
|
return jsonReply(res, 200, { ok: true, id: duplicateId, duplicate: true, duplicateOf: duplicateId });
|
|
6918
6927
|
}
|
|
6919
6928
|
recordCcTurnIfPresent(req, { kind: 'work-item', id, title: item.title, project: item.project || null });
|
|
@@ -13902,6 +13911,55 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13902
13911
|
return jsonReply(res, 200, { ok: true, cleared: cause, prId });
|
|
13903
13912
|
}},
|
|
13904
13913
|
|
|
13914
|
+
// Issue #671 — sibling to clear-paused-cause above, but for the
|
|
13915
|
+
// headSha-keyed `_buildFixIneffective` ("infra issue suspected") pause
|
|
13916
|
+
// (see engine/lifecycle.js#recordBuildFixIneffective), which is tracked
|
|
13917
|
+
// separately from `_noOpFixes` and so isn't reachable via the endpoint
|
|
13918
|
+
// above. Once a fresh CI build is queued against a rebased/retried head,
|
|
13919
|
+
// this lets an operator clear the stale pause without hand-editing
|
|
13920
|
+
// pull-requests.json.
|
|
13921
|
+
{ method: 'POST', path: '/api/pull-requests/clear-build-fix-ineffective', desc: 'Resume a paused build-fix-ineffective ("infra issue suspected") pause (issue #671) — clears _buildFixIneffective so BUILD_FAILURE auto-fix dispatch can resume', params: 'prId (canonical host:slug#number), headSha? (optional — if provided, must match the paused record\'s headSha or the clear is rejected as stale)', handler: async (req, res) => {
|
|
13922
|
+
const body = await readBody(req);
|
|
13923
|
+
const prId = typeof body?.prId === 'string' ? body.prId.trim() : '';
|
|
13924
|
+
const headSha = typeof body?.headSha === 'string' ? body.headSha.trim() : '';
|
|
13925
|
+
if (!prId) return jsonReply(res, 400, { error: 'prId required' });
|
|
13926
|
+
reloadConfig();
|
|
13927
|
+
const lifecycle = require('./engine/lifecycle');
|
|
13928
|
+
const prPaths = [
|
|
13929
|
+
...shared.getProjects(CONFIG).map(p => shared.projectPrPath(p)),
|
|
13930
|
+
shared.centralPullRequestsPath(MINIONS_DIR),
|
|
13931
|
+
];
|
|
13932
|
+
let prFound = false;
|
|
13933
|
+
let recordCleared = false;
|
|
13934
|
+
let staleHead = false;
|
|
13935
|
+
for (const prPath of prPaths) {
|
|
13936
|
+
if (recordCleared || staleHead) break;
|
|
13937
|
+
shared.mutatePullRequests(prPath, (prs) => {
|
|
13938
|
+
if (!Array.isArray(prs)) return prs;
|
|
13939
|
+
const target = prs.find(p => p && p.id === prId);
|
|
13940
|
+
if (!target) return prs;
|
|
13941
|
+
prFound = true;
|
|
13942
|
+
const record = target._buildFixIneffective;
|
|
13943
|
+
if (!record || typeof record !== 'object') return prs;
|
|
13944
|
+
// If the caller told us which head they saw the pause on (the
|
|
13945
|
+
// dashboard chip stamps this at render time), refuse to clear a
|
|
13946
|
+
// pause that has since moved on to a different head — that would
|
|
13947
|
+
// silently apply a stale click's intent to a newer commit.
|
|
13948
|
+
if (headSha && record.headSha && record.headSha !== headSha) {
|
|
13949
|
+
staleHead = true;
|
|
13950
|
+
return prs;
|
|
13951
|
+
}
|
|
13952
|
+
recordCleared = lifecycle.clearBuildFixIneffective(target);
|
|
13953
|
+
return prs;
|
|
13954
|
+
});
|
|
13955
|
+
}
|
|
13956
|
+
if (!prFound) return jsonReply(res, 404, { error: `PR ${prId} not found` });
|
|
13957
|
+
if (staleHead) return jsonReply(res, 409, { error: `build-fix-ineffective pause on PR ${prId} is for a different head than provided — refresh and retry` });
|
|
13958
|
+
if (!recordCleared) return jsonReply(res, 400, { error: `PR ${prId} has no tracked build-fix-ineffective pause` });
|
|
13959
|
+
invalidateStatusCache();
|
|
13960
|
+
return jsonReply(res, 200, { ok: true, cleared: 'build-fix-ineffective', prId });
|
|
13961
|
+
}},
|
|
13962
|
+
|
|
13905
13963
|
// ─── P-f3c9d0e7: convenience pause/resume endpoints ─────────────────────
|
|
13906
13964
|
// Single-call wrappers over `POST /api/settings { engine: { <flag>: bool } }`
|
|
13907
13965
|
// for the two operator kill-switches:
|
package/engine/consolidation.js
CHANGED
|
@@ -1153,6 +1153,52 @@ function hasReusableSignal(content) {
|
|
|
1153
1153
|
return REUSABLE_SIGNAL_RE.test(content || '');
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
|
+
// ── Engine-alert KB dedup (W-mr3pi9de, sibling of W-mr3lokxs) ────────────────
|
|
1157
|
+
// Recurring engine-system alerts (e.g. the worktree-skip-live guard firing
|
|
1158
|
+
// across many different worktrees, or the same live-checkout-dirty incident
|
|
1159
|
+
// firing every tick) used to be written as full-size KB copies with -2/-3/…
|
|
1160
|
+
// numeric suffixes via uniquePath, multiplying storage N times per day.
|
|
1161
|
+
// Instead we content-hash each alert and skip writing when a same-hash entry
|
|
1162
|
+
// already exists in the target category. The hash is stored in the entry's
|
|
1163
|
+
// frontmatter (`alertHash:`) so future passes can dedup against it cheaply.
|
|
1164
|
+
|
|
1165
|
+
// Normalized, length-bounded content hash. Collapses whitespace so trivially
|
|
1166
|
+
// re-rendered dumps (e.g. differing timestamps) still collapse to one hash.
|
|
1167
|
+
function _alertContentHash(content) {
|
|
1168
|
+
const raw = String(content || '');
|
|
1169
|
+
const normalized = raw.replace(/\s+/g, ' ').trim().slice(0, 4000);
|
|
1170
|
+
return crypto.createHash('sha256').update(normalized + ':' + raw.length).digest('hex');
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Read only the leading bytes of a file (frontmatter lives at the very top) so
|
|
1174
|
+
// scanning a directory that also holds huge legacy full-body dumps never loads
|
|
1175
|
+
// those multi-MB bodies into memory.
|
|
1176
|
+
function _readFileHead(fp, bytes = 512) {
|
|
1177
|
+
let fd;
|
|
1178
|
+
try {
|
|
1179
|
+
fd = fs.openSync(fp, 'r');
|
|
1180
|
+
const buf = Buffer.alloc(bytes);
|
|
1181
|
+
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
1182
|
+
return buf.slice(0, n).toString('utf8');
|
|
1183
|
+
} catch { return ''; }
|
|
1184
|
+
finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* best-effort */ } } }
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Does an existing KB entry in `dir` already carry this alertHash frontmatter?
|
|
1188
|
+
function _alertHashExists(dir, hash) {
|
|
1189
|
+
if (!hash || !dir || !fs.existsSync(dir)) return false;
|
|
1190
|
+
let files;
|
|
1191
|
+
try { files = fs.readdirSync(dir); } catch { return false; }
|
|
1192
|
+
const re = new RegExp(`^alertHash:\\s*${hash}\\s*$`, 'm');
|
|
1193
|
+
for (const f of files) {
|
|
1194
|
+
if (!f.endsWith('.md')) continue;
|
|
1195
|
+
const head = _readFileHead(path.join(dir, f));
|
|
1196
|
+
const fm = head.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1197
|
+
if (fm && re.test(fm[1])) return true;
|
|
1198
|
+
}
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1156
1202
|
/**
|
|
1157
1203
|
* Build a condensed KB stub for narrowly-scoped, one-off notes instead of
|
|
1158
1204
|
* duplicating the full body (issue #604 — classifyToKnowledgeBase was copying
|
|
@@ -1223,18 +1269,32 @@ async function classifyToKnowledgeBase(items, config) {
|
|
|
1223
1269
|
// Reserve full-body KB copies for content that's actually general/reusable
|
|
1224
1270
|
// (patterns, conventions, gotchas); narrowly-scoped one-off notes get a
|
|
1225
1271
|
// condensed stub + link back to the archived note instead (issue #604).
|
|
1226
|
-
|
|
1272
|
+
//
|
|
1273
|
+
// W-mr3pi9de — engine-authored system alerts (worktree-skip-live guard
|
|
1274
|
+
// fires, dirty-tree refusals, blocked dispatches, managed-spawn failures)
|
|
1275
|
+
// are operational noise, NEVER durable reusable knowledge. Force them
|
|
1276
|
+
// non-reusable regardless of category or any incidental heading-shaped
|
|
1277
|
+
// line in the body that would otherwise trip hasReusableSignal, and
|
|
1278
|
+
// content-hash-dedup recurring identical/near-identical incidents instead
|
|
1279
|
+
// of appending -2/-3 full copies with a colliding title.
|
|
1280
|
+
const isSystemAlert = shared.isEngineSystemAlert(item.name, content);
|
|
1281
|
+
const reusable = !isSystemAlert && (category === 'conventions' || hasReusableSignal(content));
|
|
1227
1282
|
const archiveRelPath = `notes/archive/${dateStamp()}-${item.name}`;
|
|
1228
1283
|
const kbBody = reusable
|
|
1229
1284
|
? content
|
|
1230
1285
|
: buildCondensedKbBody(content, titleMatch ? titleMatch[0] : null, archiveRelPath);
|
|
1231
1286
|
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1287
|
+
const alertHash = isSystemAlert ? _alertContentHash(content) : null;
|
|
1288
|
+
if (isSystemAlert && _alertHashExists(categoryDirs[category], alertHash)) {
|
|
1289
|
+
log('info', `KB dedup: skipping recurring engine-alert ${item.name} — duplicate of an existing knowledge/${category} entry`);
|
|
1290
|
+
} else {
|
|
1291
|
+
const frontmatter = `---\nsource: ${item.name}\nagent: ${agent}\ncategory: ${category}\ndate: ${dateStamp()}\nreusable: ${reusable}${isSystemAlert ? `\nalertHash: ${alertHash}` : ''}\n---\n\n`;
|
|
1292
|
+
try {
|
|
1293
|
+
safeWrite(kbPath, frontmatter + kbBody);
|
|
1294
|
+
classified++;
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
log('warn', `Failed to classify ${item.name} to knowledge base: ${err.message}`);
|
|
1297
|
+
}
|
|
1238
1298
|
}
|
|
1239
1299
|
|
|
1240
1300
|
// Per-agent memory routing — strict superset of broadcast consolidation.
|
|
@@ -1397,4 +1457,6 @@ module.exports = {
|
|
|
1397
1457
|
NOTES_MAX_BYTES,
|
|
1398
1458
|
hasReusableSignal,
|
|
1399
1459
|
buildCondensedKbBody,
|
|
1460
|
+
_alertContentHash,
|
|
1461
|
+
_alertHashExists,
|
|
1400
1462
|
};
|
package/engine/lifecycle.js
CHANGED
|
@@ -2943,6 +2943,18 @@ function recordPrNoOpFixAttempt(target, cause, source, dispatchItem, branchChang
|
|
|
2943
2943
|
return target._noOpFixes[cause];
|
|
2944
2944
|
}
|
|
2945
2945
|
|
|
2946
|
+
// #671 — Clears the `_buildFixIneffective` ("infra issue suspected") pause so
|
|
2947
|
+
// BUILD_FAILURE auto-fix dispatch can resume. Distinct from
|
|
2948
|
+
// `clearPrNoOpFixAttempt` (below) because this pause is headSha-keyed rather
|
|
2949
|
+
// than cause-keyed — see `recordBuildFixIneffective` / `shared.isBuildFixIneffectivePaused`.
|
|
2950
|
+
// Returns true when a record was actually removed so callers (the dashboard
|
|
2951
|
+
// resume endpoint) can distinguish "cleared" from "nothing to clear".
|
|
2952
|
+
function clearBuildFixIneffective(target) {
|
|
2953
|
+
if (!target?._buildFixIneffective) return false;
|
|
2954
|
+
delete target._buildFixIneffective;
|
|
2955
|
+
return true;
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2946
2958
|
function clearPrNoOpFixAttempt(target, cause) {
|
|
2947
2959
|
if (!target?._noOpFixes || !target._noOpFixes[cause]) return;
|
|
2948
2960
|
delete target._noOpFixes[cause];
|
|
@@ -6821,6 +6833,9 @@ module.exports = {
|
|
|
6821
6833
|
// Issue #2969 — exported so dashboard.js can clear a paused cause from the
|
|
6822
6834
|
// resume endpoint.
|
|
6823
6835
|
clearPrNoOpFixAttempt,
|
|
6836
|
+
// Issue #671 — exported so dashboard.js can clear the build-fix-ineffective
|
|
6837
|
+
// ("infra issue suspected") pause from the resume endpoint.
|
|
6838
|
+
clearBuildFixIneffective,
|
|
6824
6839
|
// W-mpx44p05000ze8d8 — exported so engine/ado.js + engine/github.js can
|
|
6825
6840
|
// drop stale `_noOpFixes` records during their PR status poll, and so
|
|
6826
6841
|
// unit tests can exercise the sweep helper directly.
|
package/engine/live-checkout.js
CHANGED
|
@@ -220,6 +220,25 @@ function _collectAutoCleanablePaths(dirtyFiles) {
|
|
|
220
220
|
return cleanable.length > 0 ? cleanable : null;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// W-mr3lokxs — Cap a porcelain dirty-file list before it is embedded verbatim
|
|
224
|
+
// into a live-checkout-dirty inbox alert body. On GVFS/virtual repos (see the
|
|
225
|
+
// "~120k virtual dirty files" note in prepareLiveCheckout below) a single
|
|
226
|
+
// refusal can report tens of thousands of "modified" paths; embedding the full
|
|
227
|
+
// list produced multi-MB / tens-of-MB alert bodies that then bloated the
|
|
228
|
+
// knowledge base (240 dirty-worktree dumps ≈ 113 MB). Keep the first `maxLines`
|
|
229
|
+
// entries and replace the remainder with a single truncation-notice line so the
|
|
230
|
+
// alert stays human-scannable and bounded. Pure/deterministic — exported for
|
|
231
|
+
// unit testing.
|
|
232
|
+
const DIRTY_ALERT_MAX_LINES = 200;
|
|
233
|
+
function capDirtyFileLines(dirtyFiles, maxLines = DIRTY_ALERT_MAX_LINES) {
|
|
234
|
+
const lines = Array.isArray(dirtyFiles) ? dirtyFiles : [];
|
|
235
|
+
const cap = Number.isInteger(maxLines) && maxLines > 0 ? maxLines : DIRTY_ALERT_MAX_LINES;
|
|
236
|
+
if (lines.length <= cap) return lines.slice();
|
|
237
|
+
const kept = lines.slice(0, cap);
|
|
238
|
+
kept.push(`... and ${lines.length - cap} more (run \`git status --porcelain=v1 -b\` locally for the full list)`);
|
|
239
|
+
return kept;
|
|
240
|
+
}
|
|
241
|
+
|
|
223
242
|
// issue #608 — detects a branch name that the ENGINE created for a live-mode
|
|
224
243
|
// dispatch (never the operator's own branch). Matches ONLY the literal
|
|
225
244
|
// `work/<wi-id>` convention produced by `shared.deriveWorkItemBranchName` —
|
|
@@ -1251,5 +1270,7 @@ module.exports = {
|
|
|
1251
1270
|
_looksLikeAgentBranch,
|
|
1252
1271
|
_extractBranchFromPorcelainHeader,
|
|
1253
1272
|
_commitAgentWipWithRetry,
|
|
1273
|
+
capDirtyFileLines,
|
|
1274
|
+
DIRTY_ALERT_MAX_LINES,
|
|
1254
1275
|
LIVE_CHECKOUT_AUTO_CLEAN_PATTERNS,
|
|
1255
1276
|
};
|
package/engine/shared.js
CHANGED
|
@@ -2827,11 +2827,44 @@ const KB_READABLE_CATEGORIES = Object.freeze([
|
|
|
2827
2827
|
'consolidated', 'consolidation', 'consolidations',
|
|
2828
2828
|
'team-memory', 'general', 'patterns',
|
|
2829
2829
|
]);
|
|
2830
|
+
/**
|
|
2831
|
+
* Detect engine-authored system alerts (dirty-tree refusals, worktree-skip-live
|
|
2832
|
+
* guard notices, blocked/conflicted live-checkout dispatches, managed-spawn
|
|
2833
|
+
* failures, ADO-auth alerts, …). These are operational noise, NOT durable
|
|
2834
|
+
* reusable knowledge, and the same underlying incident recurs verbatim across
|
|
2835
|
+
* many different inbox filenames (one per worktree/date), so consolidation
|
|
2836
|
+
* must never copy their full body into knowledge/ as a distinct "new" entry
|
|
2837
|
+
* per recurrence — see W-mr3pi9de (worktree-skip-live note titles were
|
|
2838
|
+
* identical across every firing regardless of worktree, producing 30+
|
|
2839
|
+
* duplicate-titled knowledge/project-notes/*.md entries on a single day) and
|
|
2840
|
+
* the sibling W-mr3lokxs fix for live-checkout-dirty alerts.
|
|
2841
|
+
*
|
|
2842
|
+
* `engine/dispatch.js#writeInboxAlert` names every file `engine-alert-<slug>-
|
|
2843
|
+
* <date>.md`, so the filename prefix is one signal. `shared.js#_writeWorktree
|
|
2844
|
+
* SkipLiveInboxNote` (and any other engine-authored note) stamps frontmatter
|
|
2845
|
+
* `agent: engine`, accepted as a second signal so alerts written under a
|
|
2846
|
+
* different filename convention are still recognized.
|
|
2847
|
+
*/
|
|
2848
|
+
function isEngineSystemAlert(name, content) {
|
|
2849
|
+
const nameLower = (name || '').toLowerCase();
|
|
2850
|
+
if (nameLower.includes('engine-alert-') || nameLower.includes('engine-worktree-skip-live-')) return true;
|
|
2851
|
+
const fmMatch = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2852
|
+
if (fmMatch && /^agent:\s*engine\s*$/im.test(fmMatch[1])) return true;
|
|
2853
|
+
return false;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2830
2856
|
/**
|
|
2831
2857
|
* Classify an inbox item into a knowledge base category.
|
|
2832
2858
|
* Single source of truth — used by consolidation.js (both LLM and regex paths).
|
|
2833
2859
|
*/
|
|
2834
2860
|
function classifyInboxItem(name, content) {
|
|
2861
|
+
// W-mr3pi9de / W-mr3lokxs — engine-authored system alerts are operational
|
|
2862
|
+
// noise. Route them to 'project-notes' BEFORE the content-substring
|
|
2863
|
+
// heuristics below, so a note whose body incidentally contains substrings
|
|
2864
|
+
// like 'lint', 'review', or 'pr-' can't be misclassified into
|
|
2865
|
+
// build-reports/reviews. classifyToKnowledgeBase additionally forces these
|
|
2866
|
+
// non-reusable and content-hash-dedups them regardless of category.
|
|
2867
|
+
if (isEngineSystemAlert(name, content)) return 'project-notes';
|
|
2835
2868
|
const nameLower = (name || '').toLowerCase();
|
|
2836
2869
|
const contentLower = (content || '').toLowerCase();
|
|
2837
2870
|
if (nameLower.includes('review') || nameLower.includes('pr-') || nameLower.includes('pr4') || nameLower.includes('feedback')) return 'reviews';
|
|
@@ -8893,7 +8926,13 @@ function _writeWorktreeSkipLiveInboxNote(worktreePath, callerTag, blockingInfo)
|
|
|
8893
8926
|
`date: ${date}`,
|
|
8894
8927
|
'---',
|
|
8895
8928
|
'',
|
|
8896
|
-
|
|
8929
|
+
// W-mr3pi9de — embed the actual worktree basename, not a hardcoded
|
|
8930
|
+
// literal. The previous fixed string made every note (and therefore
|
|
8931
|
+
// every knowledge/project-notes/*.md entry consolidation wrote from
|
|
8932
|
+
// it) share byte-identical titles regardless of which worktree fired
|
|
8933
|
+
// the guard, defeating uniquePath's -2/-3/… suffixing and flooding the
|
|
8934
|
+
// KB with near-duplicate-content entries under different numbers.
|
|
8935
|
+
`# Engine skipped worktree wipe — live dispatch guard fired (${base})`,
|
|
8897
8936
|
'',
|
|
8898
8937
|
`- caller: ${callerTag || 'unknown'}`,
|
|
8899
8938
|
`- worktree: ${worktreePath}`,
|
|
@@ -9434,6 +9473,7 @@ module.exports = {
|
|
|
9434
9473
|
KB_CATEGORIES,
|
|
9435
9474
|
KB_READABLE_CATEGORIES,
|
|
9436
9475
|
classifyInboxItem,
|
|
9476
|
+
isEngineSystemAlert,
|
|
9437
9477
|
ENGINE_DEFAULTS,
|
|
9438
9478
|
resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
|
|
9439
9479
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
|
package/engine.js
CHANGED
|
@@ -2583,7 +2583,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
2583
2583
|
'## Dirty files (`git status --porcelain=v1 -b`)',
|
|
2584
2584
|
'',
|
|
2585
2585
|
'```',
|
|
2586
|
-
...(_dirtyFiles.length > 0 ? _dirtyFiles : ['(none reported)']),
|
|
2586
|
+
...(_dirtyFiles.length > 0 ? _liveCheckout.capDirtyFileLines(_dirtyFiles) : ['(none reported)']),
|
|
2587
2587
|
'```',
|
|
2588
2588
|
'',
|
|
2589
2589
|
_autoCleanupEnabled
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2323",
|
|
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"
|