@yemi33/minions 0.1.2046 → 0.1.2047
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-prs.js +82 -2
- package/dashboard/styles.css +11 -0
- package/dashboard.js +88 -49
- package/docs/copilot-cli-schema.md +2 -1
- package/docs/runtime-adapters.md +1 -1
- package/engine/queries.js +14 -3
- package/engine/runtimes/claude.js +41 -0
- package/engine/runtimes/copilot.js +97 -3
- package/package.json +1 -1
|
@@ -22,6 +22,16 @@ function _countPrFollowups(pr) {
|
|
|
22
22
|
return n;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// W-mpmwxkzm0009ba0b — Parse a canonical PR id like `github:octo/repo#123`
|
|
26
|
+
// or `ado:org/proj/repo#456` into the {host, slug, number} triple used by
|
|
27
|
+
// POST /api/pull-requests/observe. Returns null when the id doesn't match.
|
|
28
|
+
function _parseCanonicalPrId(id) {
|
|
29
|
+
if (!id || typeof id !== 'string') return null;
|
|
30
|
+
var m = id.match(/^(github|ado):(.+)#(\d+)$/);
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
return { host: m[1], slug: m[2], number: parseInt(m[3], 10) };
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
function prRow(pr) {
|
|
26
36
|
// Minions review (agent) state — separate from ADO human review
|
|
27
37
|
const sq = pr.minionsReview || {};
|
|
@@ -61,6 +71,29 @@ function prRow(pr) {
|
|
|
61
71
|
? '<span class="pr-agent" title="' + escapeHtml(pr.reviewedBy.join(', ')) + '">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>'
|
|
62
72
|
: '<span style="color:var(--muted);font-size:11px">—</span>';
|
|
63
73
|
const createdLabel = (pr.created || '—').slice(0, 16).replace('T', ' ');
|
|
74
|
+
// Per-row auto-observe toggle (W-mpmwxkzm0009ba0b). _contextOnly === true
|
|
75
|
+
// means the engine polls status/comments but does NOT dispatch review/fix
|
|
76
|
+
// agents against this PR. Default (undefined / false) = observed.
|
|
77
|
+
var observe = pr._contextOnly !== true;
|
|
78
|
+
var observeClass = observe ? 'pr-observe-on' : 'pr-observe-off';
|
|
79
|
+
var observeLabel = observe ? 'observed' : 'context';
|
|
80
|
+
var observeTitle = observe
|
|
81
|
+
? 'Auto-observe ON — engine reviews this PR and dispatches fix agents on review/build feedback. Click to switch to context-only.'
|
|
82
|
+
: 'Context only — engine polls status and comments but will not dispatch review/fix agents. Click to enable auto-observe.';
|
|
83
|
+
// pr.id is canonical: "<host>:<slug>#<number>" (engine/shared.js getCanonicalPrId).
|
|
84
|
+
// Parse here so the toggle handler can post {host, slug, number} without
|
|
85
|
+
// shipping additional fields per row.
|
|
86
|
+
var observeBtn = '';
|
|
87
|
+
var idMatch = typeof pr.id === 'string' ? pr.id.match(/^([^:]+):(.+)#(\d+)$/) : null;
|
|
88
|
+
if (idMatch) {
|
|
89
|
+
observeBtn = '<button class="pr-observe-toggle ' + observeClass + '" '
|
|
90
|
+
+ 'data-pr-host="' + escapeHtml(idMatch[1]) + '" '
|
|
91
|
+
+ 'data-pr-slug="' + escapeHtml(idMatch[2]) + '" '
|
|
92
|
+
+ 'data-pr-number="' + escapeHtml(idMatch[3]) + '" '
|
|
93
|
+
+ 'data-pr-observe="' + (observe ? '1' : '0') + '" '
|
|
94
|
+
+ 'title="' + escapeHtml(observeTitle) + '" '
|
|
95
|
+
+ 'onclick="event.stopPropagation();togglePrObserve(this)">' + observeLabel + '</button> ';
|
|
96
|
+
}
|
|
64
97
|
// Title attrs live on the inner element (link/span/badge) so hovering the
|
|
65
98
|
// ellipsis-truncated content reveals the full text. Cell tags stay bare so
|
|
66
99
|
// the header-to-cell count assertion in test/unit.test.js continues to
|
|
@@ -74,6 +107,7 @@ function prRow(pr) {
|
|
|
74
107
|
'<td>' + reviewerCell + '</td>' +
|
|
75
108
|
'<td><span class="pr-badge ' + buildClass + '" title="' + escapeHtml(buildTitle || buildLabel) + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
|
76
109
|
'<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
110
|
+
'<td>' + (observeBtn || '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
77
111
|
'<td><span class="pr-date" title="' + escapeHtml(createdLabel) + '">' + escapeHtml(createdLabel) + '</span></td>' +
|
|
78
112
|
'<td><button class="pr-pager-btn" style="font-size:9px;padding:1px 5px;color:var(--red);border-color:var(--red)" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
|
|
79
113
|
'</tr>';
|
|
@@ -93,6 +127,7 @@ const PRS_COLGROUP =
|
|
|
93
127
|
'<col style="width:140px">' + // Signed Off By
|
|
94
128
|
'<col style="width:130px">' + // Build
|
|
95
129
|
'<col style="width:110px">' + // Status
|
|
130
|
+
'<col style="width:100px">' + // Observe
|
|
96
131
|
'<col style="width:130px">' + // Created
|
|
97
132
|
'<col style="width:50px">' + // Actions
|
|
98
133
|
'</colgroup>';
|
|
@@ -101,7 +136,7 @@ function prTableHtml(rows) {
|
|
|
101
136
|
return '<div class="pr-table-wrap pr-table-wrap--prs"><table class="pr-table pr-table--prs">' +
|
|
102
137
|
PRS_COLGROUP +
|
|
103
138
|
'<thead><tr>' +
|
|
104
|
-
'<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
|
|
139
|
+
'<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Observe</th><th>Created</th><th></th>' +
|
|
105
140
|
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
106
141
|
}
|
|
107
142
|
|
|
@@ -249,4 +284,49 @@ async function unlinkPr(id) {
|
|
|
249
284
|
} catch (e) { clearDeleted('pr:' + id); showToast('pr-toast', 'Error: ' + e.message, false); refresh(); }
|
|
250
285
|
}
|
|
251
286
|
|
|
252
|
-
|
|
287
|
+
// Per-row auto-observe toggle (W-mpmwxkzm0009ba0b). Optimistic UI: flip
|
|
288
|
+
// the button immediately, only revert on API error. The engine consumes
|
|
289
|
+
// `_contextOnly` to gate review/fix dispatch (engine/shared.js
|
|
290
|
+
// isAutoManagedPrRecord + discoverFromPrs).
|
|
291
|
+
async function togglePrObserve(btn) {
|
|
292
|
+
if (!btn || btn.disabled) return;
|
|
293
|
+
const host = btn.dataset.prHost;
|
|
294
|
+
const slug = btn.dataset.prSlug;
|
|
295
|
+
const number = parseInt(btn.dataset.prNumber, 10);
|
|
296
|
+
if (!host || !slug || !Number.isFinite(number)) return;
|
|
297
|
+
const wasObserve = btn.dataset.prObserve === '1';
|
|
298
|
+
const next = !wasObserve;
|
|
299
|
+
// Optimistic flip
|
|
300
|
+
btn.dataset.prObserve = next ? '1' : '0';
|
|
301
|
+
btn.classList.toggle('pr-observe-on', next);
|
|
302
|
+
btn.classList.toggle('pr-observe-off', !next);
|
|
303
|
+
btn.textContent = next ? 'observed' : 'context';
|
|
304
|
+
btn.disabled = true;
|
|
305
|
+
try {
|
|
306
|
+
const res = await fetch('/api/pull-requests/observe', {
|
|
307
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
308
|
+
body: JSON.stringify({ host, slug, number, observe: next })
|
|
309
|
+
});
|
|
310
|
+
if (!res.ok) {
|
|
311
|
+
const d = await res.json().catch(() => ({}));
|
|
312
|
+
// Revert
|
|
313
|
+
btn.dataset.prObserve = wasObserve ? '1' : '0';
|
|
314
|
+
btn.classList.toggle('pr-observe-on', wasObserve);
|
|
315
|
+
btn.classList.toggle('pr-observe-off', !wasObserve);
|
|
316
|
+
btn.textContent = wasObserve ? 'observed' : 'context';
|
|
317
|
+
showToast('pr-toast', 'Failed: ' + (d.error || 'unknown'), false);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
showToast('pr-toast', next ? 'Auto-observe enabled' : 'Switched to context only', true);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
btn.dataset.prObserve = wasObserve ? '1' : '0';
|
|
323
|
+
btn.classList.toggle('pr-observe-on', wasObserve);
|
|
324
|
+
btn.classList.toggle('pr-observe-off', !wasObserve);
|
|
325
|
+
btn.textContent = wasObserve ? 'observed' : 'context';
|
|
326
|
+
showToast('pr-toast', 'Error: ' + e.message, false);
|
|
327
|
+
} finally {
|
|
328
|
+
btn.disabled = false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, unlinkPr, togglePrObserve };
|
package/dashboard/styles.css
CHANGED
|
@@ -322,6 +322,17 @@
|
|
|
322
322
|
.pr-page-info { font-size: var(--text-base); color: var(--muted); }
|
|
323
323
|
.pr-date { font-size: var(--text-base); color: var(--muted); }
|
|
324
324
|
|
|
325
|
+
.pr-observe-toggle {
|
|
326
|
+
font-size: 9px; padding: 1px 6px; border-radius: var(--radius-sm); cursor: pointer;
|
|
327
|
+
text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;
|
|
328
|
+
background: var(--surface2); transition: all var(--transition-base);
|
|
329
|
+
}
|
|
330
|
+
.pr-observe-toggle:disabled { opacity: 0.6; cursor: wait; }
|
|
331
|
+
.pr-observe-toggle.pr-observe-on { color: var(--green); border: 1px solid var(--green); background: rgba(63,185,80,0.12); }
|
|
332
|
+
.pr-observe-toggle.pr-observe-on:hover:not(:disabled) { background: rgba(63,185,80,0.22); }
|
|
333
|
+
.pr-observe-toggle.pr-observe-off { color: var(--muted); border: 1px dashed var(--border); }
|
|
334
|
+
.pr-observe-toggle.pr-observe-off:hover:not(:disabled) { color: var(--text); border-color: var(--text); }
|
|
335
|
+
|
|
325
336
|
.archive-btn {
|
|
326
337
|
background: var(--surface2); border: 1px solid var(--border); color: var(--muted);
|
|
327
338
|
font-size: var(--text-base); padding: var(--space-2) var(--space-5); border-radius: var(--radius-sm); cursor: pointer; transition: all var(--transition-base); margin-left: auto;
|
package/dashboard.js
CHANGED
|
@@ -855,6 +855,67 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
855
855
|
return { ...result, prPath, targetProject, projectResolution, prNum };
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
// W-mpmwxkzm0009ba0b — Per-row auto-observe toggle backing helper for
|
|
859
|
+
// POST /api/pull-requests/observe. Flips `_contextOnly` / `_autoObserve` on
|
|
860
|
+
// an existing tracked PR record under a lock (per CLAUDE.md mutate convention).
|
|
861
|
+
// Body shape: { host: 'github'|'ado', slug, number, observe: boolean }.
|
|
862
|
+
// Returns the updated record + the PR path that was touched. Throws an
|
|
863
|
+
// Error with `statusCode` for the route handler to map to an HTTP status.
|
|
864
|
+
function updatePullRequestObserveFlag({ host, slug, number, observe } = {}, config = CONFIG, minionsDir = MINIONS_DIR) {
|
|
865
|
+
const hostStr = String(host || '').trim().toLowerCase();
|
|
866
|
+
const slugStr = String(slug || '').trim();
|
|
867
|
+
const numberInt = Number.parseInt(number, 10);
|
|
868
|
+
if (!hostStr || (hostStr !== 'github' && hostStr !== 'ado')) {
|
|
869
|
+
const err = new Error('host must be "github" or "ado"');
|
|
870
|
+
err.statusCode = 400;
|
|
871
|
+
throw err;
|
|
872
|
+
}
|
|
873
|
+
if (!slugStr) {
|
|
874
|
+
const err = new Error('slug required');
|
|
875
|
+
err.statusCode = 400;
|
|
876
|
+
throw err;
|
|
877
|
+
}
|
|
878
|
+
if (!Number.isFinite(numberInt) || numberInt <= 0) {
|
|
879
|
+
const err = new Error('number must be a positive integer');
|
|
880
|
+
err.statusCode = 400;
|
|
881
|
+
throw err;
|
|
882
|
+
}
|
|
883
|
+
if (typeof observe !== 'boolean') {
|
|
884
|
+
const err = new Error('observe must be a boolean');
|
|
885
|
+
err.statusCode = 400;
|
|
886
|
+
throw err;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const canonicalId = `${hostStr}:${slugStr}#${numberInt}`;
|
|
890
|
+
const projects = shared.getProjects(config);
|
|
891
|
+
const prPaths = [
|
|
892
|
+
...projects.map(p => shared.projectPrPath(p)),
|
|
893
|
+
shared.centralPullRequestsPath(minionsDir),
|
|
894
|
+
];
|
|
895
|
+
|
|
896
|
+
let updated = null;
|
|
897
|
+
let updatedPath = null;
|
|
898
|
+
for (const prPath of prPaths) {
|
|
899
|
+
if (updated) break;
|
|
900
|
+
shared.mutatePullRequests(prPath, (prs) => {
|
|
901
|
+
const pr = prs.find(p => p && p.id === canonicalId);
|
|
902
|
+
if (!pr) return prs;
|
|
903
|
+
pr._contextOnly = !observe;
|
|
904
|
+
pr._autoObserve = !!observe;
|
|
905
|
+
updated = { id: pr.id, _contextOnly: pr._contextOnly, _autoObserve: pr._autoObserve };
|
|
906
|
+
updatedPath = prPath;
|
|
907
|
+
return prs;
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (!updated) {
|
|
912
|
+
const err = new Error(`pull request ${canonicalId} not found`);
|
|
913
|
+
err.statusCode = 404;
|
|
914
|
+
throw err;
|
|
915
|
+
}
|
|
916
|
+
return { ...updated, prPath: updatedPath };
|
|
917
|
+
}
|
|
918
|
+
|
|
858
919
|
function _normalizeSkillDirForCompare(dir) {
|
|
859
920
|
const resolved = path.resolve(String(dir || '').replace(/\//g, path.sep));
|
|
860
921
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
@@ -1173,48 +1234,13 @@ function _getDashboardBrowserPresence(now = Date.now()) {
|
|
|
1173
1234
|
return { active: activeTabs.length > 0, activeTabs: activeTabs.length, maxAgeMs: DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS };
|
|
1174
1235
|
}
|
|
1175
1236
|
|
|
1176
|
-
// Hot-reload: watch
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
HTML_RAW = newRaw;
|
|
1184
|
-
HTML = HTML_RAW;
|
|
1185
|
-
HTML_GZ = zlib.gzipSync(HTML);
|
|
1186
|
-
HTML_ETAG = '"' + require('crypto').createHash('md5').update(HTML).digest('hex') + '"';
|
|
1187
|
-
// Bust the /api/status cache so the new dashboardBuildId propagates on the
|
|
1188
|
-
// next poll — refresh.js compares it against its first-observed value and
|
|
1189
|
-
// hard-reloads on mismatch (R3, W-mpgb0xgc000hf1d3).
|
|
1190
|
-
try { invalidateStatusCache(); } catch { /* status cache may not be initialized yet */ }
|
|
1191
|
-
console.log(' Dashboard hot-reloaded');
|
|
1192
|
-
// Push reload to all connected browsers via status-stream (saves a connection)
|
|
1193
|
-
for (const res of _statusStreamClients) {
|
|
1194
|
-
try { res.write('event: reload\ndata: reload\n\n'); } catch { _removeSseClient(_statusStreamClients, res); }
|
|
1195
|
-
}
|
|
1196
|
-
// Legacy hot-reload clients
|
|
1197
|
-
for (const res of _hotReloadClients) {
|
|
1198
|
-
try { res.write('data: reload\n\n'); } catch { _removeSseClient(_hotReloadClients, res); }
|
|
1199
|
-
}
|
|
1200
|
-
} catch (e) { console.error(' Hot-reload error:', e.message); }
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const dashDir = path.join(MINIONS_DIR, 'dashboard');
|
|
1204
|
-
if (fs.existsSync(dashDir)) {
|
|
1205
|
-
let _reloadTimer = null;
|
|
1206
|
-
const scheduleReload = () => {
|
|
1207
|
-
if (_reloadTimer) clearTimeout(_reloadTimer);
|
|
1208
|
-
_reloadTimer = setTimeout(rebuildDashboardHtml, 300); // debounce 300ms
|
|
1209
|
-
};
|
|
1210
|
-
// Watch top-level files (styles.css, layout.html)
|
|
1211
|
-
try { fs.watch(dashDir, scheduleReload); } catch { /* optional */ }
|
|
1212
|
-
// Watch subdirectories (pages/, js/)
|
|
1213
|
-
for (const sub of ['pages', 'js']) {
|
|
1214
|
-
const subDir = path.join(dashDir, sub);
|
|
1215
|
-
if (fs.existsSync(subDir)) try { fs.watch(subDir, scheduleReload); } catch { /* optional */ }
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1237
|
+
// Hot-reload removed (2026-05-26): file-watch-driven rebuilds force-reloaded
|
|
1238
|
+
// every connected browser on any change under dashboard/, which surprised
|
|
1239
|
+
// users when parallel agent merges touched dashboard.js or dashboard/js/*.
|
|
1240
|
+
// The dashboard HTML is now assembled once at process boot. Code changes on
|
|
1241
|
+
// disk land in the running process only after an explicit `minions restart`;
|
|
1242
|
+
// the buildId/startedAt mismatch in refresh.js then triggers a one-shot
|
|
1243
|
+
// browser reload on that next poll — driven by user-initiated restart only.
|
|
1218
1244
|
|
|
1219
1245
|
// -- Data Collectors (most moved to engine/queries.js) --
|
|
1220
1246
|
|
|
@@ -4095,7 +4121,13 @@ function _recoverPartialDocChatResponse(result, sessionKey) {
|
|
|
4095
4121
|
|
|
4096
4122
|
function _shouldSuppressDocChatPostPatchError(ccError, finalize) {
|
|
4097
4123
|
if (!finalize || finalize.edited !== true) return false;
|
|
4098
|
-
if (!ccError
|
|
4124
|
+
if (!ccError) return false;
|
|
4125
|
+
// W-mpmwxni2000c25c7-a — accept both the legacy 'unknown-model' errorClass
|
|
4126
|
+
// (still emitted by the dashboard preflight at _preflightModelCheck) and
|
|
4127
|
+
// the new 'model-unavailable' code returned by copilot.parseError for
|
|
4128
|
+
// invalid-model responses. Suppress in either case — the edit already
|
|
4129
|
+
// landed, so the stale model error shouldn't surface to the user.
|
|
4130
|
+
if (ccError.errorClass !== 'unknown-model' && ccError.errorClass !== 'model-unavailable') return false;
|
|
4099
4131
|
return String(ccError.runtime || '').toLowerCase() === 'copilot';
|
|
4100
4132
|
}
|
|
4101
4133
|
|
|
@@ -9994,12 +10026,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9994
10026
|
{ method: 'GET', path: '/api/qa/runs', desc: 'List QA validation runs (newest first). Optional ?limit=N and ?status=pending|running|passed|failed|errored filters.', handler: handleQaRunsList },
|
|
9995
10027
|
{ method: 'GET', path: /^\/api\/qa\/runs\/([^/?]+)$/, template: '/api/qa/runs/<id>', desc: 'Fetch a single QA run record by id.', handler: handleQaRunsById },
|
|
9996
10028
|
{ method: 'GET', path: /^\/api\/qa\/artifacts\/([^/?]+)\/([^?]+)$/, template: '/api/qa/artifacts/<runId>/<file>', desc: 'Serve a QA artifact file (image/video/log). Sandboxed to engine/qa-artifacts/; rejects path traversal with 403.', handler: handleQaArtifact },
|
|
9997
|
-
{ method: 'GET', path: '/api/hot-reload', desc: 'SSE stream for dashboard hot-reload notifications', handler: (req, res) => {
|
|
9998
|
-
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
9999
|
-
res.write('data: connected\n\n');
|
|
10000
|
-
_trackSseClient(_hotReloadClients, req, res);
|
|
10001
|
-
}},
|
|
10002
|
-
|
|
10003
10029
|
// QA Runbooks (W-mpeiwz6k0005bf34-a) — per-project test plans stored at
|
|
10004
10030
|
// <MINIONS_DIR>/projects/<name>/runbooks/<id>.json. Pure persistence —
|
|
10005
10031
|
// dispatch + run records + UI live in follow-up plan items.
|
|
@@ -10222,6 +10248,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
10222
10248
|
})();
|
|
10223
10249
|
}},
|
|
10224
10250
|
|
|
10251
|
+
{ method: 'POST', path: '/api/pull-requests/observe', desc: 'Toggle auto-observe (_contextOnly flag) on a tracked PR', params: 'host (github|ado), slug, number, observe (boolean)', handler: async (req, res) => {
|
|
10252
|
+
const body = await readBody(req);
|
|
10253
|
+
reloadConfig();
|
|
10254
|
+
try {
|
|
10255
|
+
const result = updatePullRequestObserveFlag(body, CONFIG);
|
|
10256
|
+
invalidateStatusCache();
|
|
10257
|
+
return jsonReply(res, 200, { ok: true, ...result, observe: !result._contextOnly });
|
|
10258
|
+
} catch (e) {
|
|
10259
|
+
return jsonReply(res, e.statusCode || 400, { error: e.message });
|
|
10260
|
+
}
|
|
10261
|
+
}},
|
|
10262
|
+
|
|
10225
10263
|
{ method: 'POST', path: '/api/pull-requests/delete', desc: 'Remove a PR from tracking', params: 'id, project?', handler: async (req, res) => {
|
|
10226
10264
|
const body = await readBody(req);
|
|
10227
10265
|
const { id } = body;
|
|
@@ -10831,6 +10869,7 @@ module.exports = {
|
|
|
10831
10869
|
_buildDocChatResponsePayload,
|
|
10832
10870
|
_inferDocChatProject,
|
|
10833
10871
|
_linkPullRequestForTracking: linkPullRequestForTracking,
|
|
10872
|
+
_updatePullRequestObserveFlag: updatePullRequestObserveFlag,
|
|
10834
10873
|
_resolveSkillReadPath,
|
|
10835
10874
|
// Per-CC-turn correlation surface
|
|
10836
10875
|
_ccTurnCreations,
|
|
@@ -614,8 +614,9 @@ When implementing `engine/runtimes/copilot.js`:
|
|
|
614
614
|
should still parse cleanly — let the consumer decide to ignore.
|
|
615
615
|
9. `parseError(rawOutput)` patterns:
|
|
616
616
|
- `auth-failure`: `/not authenticated|copilot login|401|403/i`
|
|
617
|
+
- `model-unavailable` (retriable=false): `/unknown model|model not found|invalid model|model_not_found|400.*model/i` — message includes the cached model catalog (`_warmModelCache` populates `_modelDiscoveryResults` from `listModels()` ahead of time so the error path stays sync). Falls back to "Configure a valid model in Settings → Engine." when the cache is empty.
|
|
618
|
+
- `model-unavailable` (retriable=true): `/overloaded_error|service_unavailable|503|temporarily unavailable/i` — engine retries with `engine.copilotFallbackModel`.
|
|
617
619
|
- `rate-limit`: `/rate limit|too many requests|429/i`
|
|
618
|
-
- `unknown-model`: `/unknown model|model not found|model.*invalid/i`
|
|
619
620
|
- `crash`: `/internal error|panic|uncaught/i`
|
|
620
621
|
10. `listModels()` per §6 — return `null` on any failure (network, parse, auth).
|
|
621
622
|
`modelsCache` path: `engine/copilot-models.json`.
|
package/docs/runtime-adapters.md
CHANGED
|
@@ -49,7 +49,7 @@ methods that genuinely differ.
|
|
|
49
49
|
| `modelLooksFamiliar(model)` | boolean | Heuristic powering the preflight "stale model after CLI switch" warning. |
|
|
50
50
|
| `parseOutput(raw)` | `{ text, usage, sessionId, model }` | Final-event parser. |
|
|
51
51
|
| `parseStreamChunk(line)` | event object or null | Single JSONL line → typed event. |
|
|
52
|
-
| `parseError(rawOutput)` | `{ message, code, retriable }` | Codes: `auth-failure`, `context-limit`, `budget-exceeded`, `crash`, null. |
|
|
52
|
+
| `parseError(rawOutput)` | `{ message, code, retriable }` | Codes: `auth-failure`, `context-limit`, `budget-exceeded`, `model-unavailable` (retriable=true for upstream overload/503; retriable=false for invalid/typo'd model id — Copilot enriches the message via `_warmModelCache()` so it lists the available models), `crash`, null. |
|
|
53
53
|
| `createStreamConsumer(ctx)` | consumer object | Stream accumulator used by `engine/llm.js`. |
|
|
54
54
|
| `detectPermissionGate`, `getPromptDeliveryMode`, `usesSystemPromptFile`, `classifyFailure` | misc | Adapter-owned policy that engine code reads through accessors instead of branching on `runtime.name`. |
|
|
55
55
|
|
package/engine/queries.js
CHANGED
|
@@ -1930,14 +1930,25 @@ function getProjectGitStatus(localPath, configuredMainBranch = null) {
|
|
|
1930
1930
|
// the pre-pull ahead/behind counts for up to 15s + one SPA poll (~19s
|
|
1931
1931
|
// user-visible lag) because the rebuilt fast-state still hits this
|
|
1932
1932
|
// cache and never schedules a refresh until the TTL itself expires.
|
|
1933
|
-
|
|
1933
|
+
// Revalidate a cached MISSING value via a cheap existsSync. The snapshot-
|
|
1934
|
+
// based freshness check below can't detect "directory came back" because
|
|
1935
|
+
// there was no `.git` to snapshot when we wrote MISSING — without this
|
|
1936
|
+
// gate the cache pins MISSING for the full 15s TTL after the path is
|
|
1937
|
+
// recreated.
|
|
1938
|
+
const cachedIsMissing = cached && cached.value === PROJECT_GIT_STATUS_MISSING;
|
|
1939
|
+
if (cachedIsMissing && fs.existsSync(localPath)) {
|
|
1940
|
+
// Path came back — fall through to schedule a fresh probe.
|
|
1941
|
+
} else if (cached && cached.ts && (now - cached.ts) < PROJECT_GIT_STATUS_TTL
|
|
1934
1942
|
&& !_projectGitRefsAdvancedSince(localPath, configuredMainBranch, cached.refMtimes)) {
|
|
1935
1943
|
return cached.value;
|
|
1936
1944
|
}
|
|
1937
1945
|
// Cheap synchronous existsSync — short-circuits a path that just disappeared
|
|
1938
|
-
// (project removed) without scheduling a useless git probe.
|
|
1946
|
+
// (project removed) without scheduling a useless git probe. `refMtimes: null`
|
|
1947
|
+
// keeps the entry shape uniform with entries produced by
|
|
1948
|
+
// `_scheduleProjectGitStatusRefresh` so the freshness check above always
|
|
1949
|
+
// sees a defined field.
|
|
1939
1950
|
if (!fs.existsSync(localPath)) {
|
|
1940
|
-
_projectGitStatusCache.set(key, { ts: now, value: PROJECT_GIT_STATUS_MISSING, promise: null });
|
|
1951
|
+
_projectGitStatusCache.set(key, { ts: now, value: PROJECT_GIT_STATUS_MISSING, promise: null, refMtimes: null });
|
|
1941
1952
|
return PROJECT_GIT_STATUS_MISSING;
|
|
1942
1953
|
}
|
|
1943
1954
|
// Stale or never-populated — kick off a background refresh and return the
|
|
@@ -522,6 +522,27 @@ function parseStreamChunk(line) {
|
|
|
522
522
|
|
|
523
523
|
// ── Error Normalization ──────────────────────────────────────────────────────
|
|
524
524
|
|
|
525
|
+
// Pull a model id out of an Anthropic-style invalid-model error string. Claude
|
|
526
|
+
// CLI surfaces these as either plain stderr ("Unknown model: claude-x") or as
|
|
527
|
+
// the API's structured error body ({"type":"error","error":{"message":
|
|
528
|
+
// "model: claude-x: not a valid model"}}). Returns null when no obvious model
|
|
529
|
+
// token is present — parseError falls back to "unknown" in that case.
|
|
530
|
+
function _extractInvalidModelName(text) {
|
|
531
|
+
if (!text) return null;
|
|
532
|
+
const patterns = [
|
|
533
|
+
/(?:unknown|invalid)\s+model(?:\s+id)?\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
|
|
534
|
+
/model\s+['"`]([^'"`]+)['"`]\s+(?:not\s+found|is\s+invalid|is\s+unknown|invalid)/i,
|
|
535
|
+
/model\s*[:=]\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?\s*[:,]\s*(?:not\s+a\s+valid\s+model|not\s+found|invalid)/i,
|
|
536
|
+
/model\s+(?:not\s+found|invalid|unknown)\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
|
|
537
|
+
/"model"\s*:\s*"([^"]+)"/i,
|
|
538
|
+
];
|
|
539
|
+
for (const re of patterns) {
|
|
540
|
+
const m = text.match(re);
|
|
541
|
+
if (m && m[1]) return m[1];
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
525
546
|
/**
|
|
526
547
|
* Inspect raw agent output (stdout/stderr concatenated by the caller) and map
|
|
527
548
|
* common Claude error patterns onto a normalized shape:
|
|
@@ -531,6 +552,11 @@ function parseStreamChunk(line) {
|
|
|
531
552
|
* - 'auth-failure' — invalid API key / credit-card / org-blocked
|
|
532
553
|
* - 'context-limit' — context window exhausted
|
|
533
554
|
* - 'budget-exceeded' — `--max-budget-usd` ceiling hit
|
|
555
|
+
* - 'model-unavailable' — model not available. Two flavors distinguished by
|
|
556
|
+
* `retriable`: retriable=true for transient upstream
|
|
557
|
+
* overload (503/overloaded_error — engine retries
|
|
558
|
+
* with `--fallback-model`); retriable=false for an
|
|
559
|
+
* invalid/typo'd model id (user must fix config).
|
|
534
560
|
* - 'crash' — CLI crashed (segfault, panic, "Internal error")
|
|
535
561
|
* - null — no recognised pattern
|
|
536
562
|
*
|
|
@@ -553,6 +579,20 @@ function parseError(rawOutput) {
|
|
|
553
579
|
if (/budget.*exceed|max.budget.usd.*reach|cost.*limit.*exceed/i.test(lower)) {
|
|
554
580
|
return { message: 'Claude budget cap exceeded — check your Claude account spending limit.', code: 'budget-exceeded', retriable: false };
|
|
555
581
|
}
|
|
582
|
+
// W-mpmwxni2000c25c7-a — Configured model isn't a valid Anthropic id (typo,
|
|
583
|
+
// deprecated id, invalid_request_error on `model:` field). Claude has no
|
|
584
|
+
// listModels() catalog (capabilities.modelDiscovery === false), so the
|
|
585
|
+
// message points the operator at Settings instead of enumerating ids.
|
|
586
|
+
// Match BEFORE the overload branch — the overload regex matches
|
|
587
|
+
// `model.*unavailable` and would otherwise swallow "model X is invalid".
|
|
588
|
+
if (/unknown\s+model|model\s+not\s+found|invalid\s+model|model_not_found|not\s+a\s+valid\s+model|400[^a-z]+(?:bad\s+request|invalid|model)/i.test(text)) {
|
|
589
|
+
const name = _extractInvalidModelName(text) || 'unknown';
|
|
590
|
+
return {
|
|
591
|
+
message: `Model "${name}" not available for runtime claude. Configure a valid model in Settings → Engine.`,
|
|
592
|
+
code: 'model-unavailable',
|
|
593
|
+
retriable: false,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
556
596
|
// W-mpg6isvy000xca4d — Anthropic overload / 503 / service-unavailable. Claude's
|
|
557
597
|
// own `--fallback-model` only fires on 429 (rate-limit); these failure modes
|
|
558
598
|
// hang the agent until the 5h timeout. Classify as MODEL_UNAVAILABLE so the
|
|
@@ -828,5 +868,6 @@ module.exports = {
|
|
|
828
868
|
permissionBypassFlags: PERMISSION_BYPASS_FLAGS,
|
|
829
869
|
// Exposed for unit tests — never imported by engine code
|
|
830
870
|
_CLAUDE_SHORTHANDS,
|
|
871
|
+
_extractInvalidModelName,
|
|
831
872
|
THINKING_BLOCK_TYPES,
|
|
832
873
|
};
|
|
@@ -785,6 +785,58 @@ function parseStreamChunk(line) {
|
|
|
785
785
|
|
|
786
786
|
// ── Error Normalization ─────────────────────────────────────────────────────
|
|
787
787
|
|
|
788
|
+
// In-memory model-discovery cache used by parseError's invalid-model branch
|
|
789
|
+
// (W-mpmwxni2000c25c7-a). parseError is synchronous — to enrich the error
|
|
790
|
+
// message with the live model catalog without adding a per-error HTTP round
|
|
791
|
+
// trip, the engine pre-warms this cache via `_warmModelCache()` (e.g. during
|
|
792
|
+
// preflight or the first listModels() call from the dashboard). Concurrent
|
|
793
|
+
// callers share the in-flight promise so the API is only hit once even when
|
|
794
|
+
// multiple agents fail simultaneously. `_modelDiscoveryResults` stores the
|
|
795
|
+
// resolved catalog (or `[]` on failure); parseError reads it synchronously.
|
|
796
|
+
let _modelDiscoveryResults = null;
|
|
797
|
+
let _modelDiscoveryPromise = null;
|
|
798
|
+
|
|
799
|
+
async function _warmModelCache(opts = {}) {
|
|
800
|
+
if (_modelDiscoveryPromise) return _modelDiscoveryPromise;
|
|
801
|
+
if (Array.isArray(_modelDiscoveryResults)) return _modelDiscoveryResults;
|
|
802
|
+
_modelDiscoveryPromise = listModels(opts).then(
|
|
803
|
+
(models) => { _modelDiscoveryResults = Array.isArray(models) ? models : []; return _modelDiscoveryResults; },
|
|
804
|
+
() => { _modelDiscoveryResults = []; return _modelDiscoveryResults; },
|
|
805
|
+
);
|
|
806
|
+
try {
|
|
807
|
+
return await _modelDiscoveryPromise;
|
|
808
|
+
} finally {
|
|
809
|
+
_modelDiscoveryPromise = null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function _resetModelCache({ models = null } = {}) {
|
|
814
|
+
_modelDiscoveryResults = Array.isArray(models) ? models : (models === null ? null : []);
|
|
815
|
+
_modelDiscoveryPromise = null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Pull a model id out of a Copilot/Anthropic-style invalid-model error string.
|
|
819
|
+
// Handles the shapes observed in the wild:
|
|
820
|
+
// "Unknown model: banana"
|
|
821
|
+
// "model not found: gpt-5.4"
|
|
822
|
+
// "model 'gpt-x' is invalid"
|
|
823
|
+
// "Invalid model id: claude-sonnet"
|
|
824
|
+
// {"error":"model_not_found","model":"gpt-x"} / {"model":"gpt-x"}
|
|
825
|
+
function _extractInvalidModelName(text) {
|
|
826
|
+
if (!text) return null;
|
|
827
|
+
const patterns = [
|
|
828
|
+
/(?:unknown|invalid)\s+model(?:\s+id)?\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
|
|
829
|
+
/model\s+['"`]([^'"`]+)['"`]\s+(?:not\s+found|is\s+invalid|is\s+unknown|invalid)/i,
|
|
830
|
+
/model\s+(?:not\s+found|invalid|unknown)\s*[:=]?\s*['"`]?([A-Za-z0-9._\/-]+)['"`]?/i,
|
|
831
|
+
/"model"\s*:\s*"([^"]+)"/i,
|
|
832
|
+
];
|
|
833
|
+
for (const re of patterns) {
|
|
834
|
+
const m = text.match(re);
|
|
835
|
+
if (m && m[1]) return m[1];
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
|
|
788
840
|
function _collectErrorSignal(rawOutput) {
|
|
789
841
|
const text = rawOutput == null ? '' : String(rawOutput);
|
|
790
842
|
if (!text) return '';
|
|
@@ -835,6 +887,27 @@ function _collectErrorSignal(rawOutput) {
|
|
|
835
887
|
return sawJsonLine ? '' : text;
|
|
836
888
|
}
|
|
837
889
|
|
|
890
|
+
function _readModelIdsFromDiskCache() {
|
|
891
|
+
try {
|
|
892
|
+
const text = fs.readFileSync(MODELS_CACHE, 'utf8');
|
|
893
|
+
const obj = JSON.parse(text);
|
|
894
|
+
if (!obj || !Array.isArray(obj.models)) return null;
|
|
895
|
+
const ids = obj.models.map(m => m && m.id ? String(m.id) : null).filter(Boolean);
|
|
896
|
+
return ids.length > 0 ? ids : null;
|
|
897
|
+
} catch { return null; }
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function _resolveCachedModelIds() {
|
|
901
|
+
// In-memory cache (seeded by `_warmModelCache()` or test `_resetModelCache`)
|
|
902
|
+
// wins over disk so unit tests stay hermetic. Production code that hasn't
|
|
903
|
+
// warmed the in-memory cache still gets the catalog via the disk file that
|
|
904
|
+
// `engine/model-discovery.js#getRuntimeModels` populates during preflight.
|
|
905
|
+
if (Array.isArray(_modelDiscoveryResults) && _modelDiscoveryResults.length > 0) {
|
|
906
|
+
return _modelDiscoveryResults.map(m => m && m.id ? String(m.id) : null).filter(Boolean);
|
|
907
|
+
}
|
|
908
|
+
return _readModelIdsFromDiskCache();
|
|
909
|
+
}
|
|
910
|
+
|
|
838
911
|
function parseError(rawOutput) {
|
|
839
912
|
const text = _collectErrorSignal(rawOutput);
|
|
840
913
|
if (!text) return { message: '', code: null, retriable: true };
|
|
@@ -845,6 +918,24 @@ function parseError(rawOutput) {
|
|
|
845
918
|
if (hasExplicitAuthFailure || hasAuthStatusCode) {
|
|
846
919
|
return { message: text, code: 'auth-failure', retriable: false };
|
|
847
920
|
}
|
|
921
|
+
// W-mpmwxni2000c25c7-a — Configured model isn't in the Copilot catalog
|
|
922
|
+
// (typo, deprecated id, 400-style invalid-model response). Classify as
|
|
923
|
+
// MODEL_UNAVAILABLE with retriable: false so the engine surfaces an
|
|
924
|
+
// actionable error instead of looping the dispatch. Must match BEFORE the
|
|
925
|
+
// overload branch — the overload regex catches `model.*unavailable`,
|
|
926
|
+
// which would otherwise swallow legitimate "model not found" responses.
|
|
927
|
+
if (/unknown\s+model|model\s+not\s+found|invalid\s+model|model_not_found|400[^a-z]+(?:bad\s+request|invalid|model)|model.*\b400\b/i.test(text)) {
|
|
928
|
+
const name = _extractInvalidModelName(text) || 'unknown';
|
|
929
|
+
const ids = _resolveCachedModelIds();
|
|
930
|
+
const tail = (ids && ids.length > 0)
|
|
931
|
+
? `Available models: ${ids.join(', ')}`
|
|
932
|
+
: 'Configure a valid model in Settings → Engine.';
|
|
933
|
+
return {
|
|
934
|
+
message: `Model "${name}" not available for runtime copilot. ${tail}`,
|
|
935
|
+
code: 'model-unavailable',
|
|
936
|
+
retriable: false,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
848
939
|
// W-mpg6isvy000xca4d — Copilot has no --fallback-model flag; classify
|
|
849
940
|
// overloaded / 503 / service_unavailable as MODEL_UNAVAILABLE so the engine
|
|
850
941
|
// retry can OVERRIDE --model with engine.copilotFallbackModel. Match before
|
|
@@ -856,9 +947,6 @@ function parseError(rawOutput) {
|
|
|
856
947
|
if (/rate limit|too many requests|\b429\b/i.test(text)) {
|
|
857
948
|
return { message: text, code: 'rate-limit', retriable: true };
|
|
858
949
|
}
|
|
859
|
-
if (/unknown model|model not found|model.*invalid|invalid model/i.test(text)) {
|
|
860
|
-
return { message: text, code: 'unknown-model', retriable: false };
|
|
861
|
-
}
|
|
862
950
|
if (/budget.*exceed|premium.*limit.*reach|quota.*exceed/i.test(lower)) {
|
|
863
951
|
return { message: text, code: 'budget-exceeded', retriable: false };
|
|
864
952
|
}
|
|
@@ -1198,6 +1286,12 @@ module.exports = {
|
|
|
1198
1286
|
_pickStandaloneCopilotFromOutput,
|
|
1199
1287
|
_resolveNpmCopilotJsEntry,
|
|
1200
1288
|
_isCachedBinUsable,
|
|
1289
|
+
// W-mpmwxni2000c25c7-a — invalid-model error-path helpers. `_warmModelCache`
|
|
1290
|
+
// populates the in-memory model catalog so parseError can enrich its
|
|
1291
|
+
// "Model X not available" message without a per-error HTTP round trip.
|
|
1292
|
+
_warmModelCache,
|
|
1293
|
+
_resetModelCache,
|
|
1294
|
+
_extractInvalidModelName,
|
|
1201
1295
|
CAPS_SCHEMA_VERSION,
|
|
1202
1296
|
KNOWN_EVENT_TYPES,
|
|
1203
1297
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2047",
|
|
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"
|