@yemi33/minions 0.1.2081 → 0.1.2083
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/qa.js +41 -2
- package/dashboard/js/refresh.js +81 -64
- package/dashboard/js/settings.js +56 -56
- package/dashboard/js/state.js +1 -1
- package/dashboard/pages/qa.html +3 -2
- package/dashboard.js +35 -6
- package/docs/qa-runbook-lifecycle.md +21 -1
- package/engine/features.js +7 -2
- package/engine/playbook.js +8 -0
- package/engine/qa-sessions.js +316 -42
- package/engine.js +17 -0
- package/package.json +1 -1
- package/playbooks/qa-session-setup.md +32 -0
- package/prompts/cc-system.md +14 -6
package/dashboard/js/qa.js
CHANGED
|
@@ -504,6 +504,7 @@ function qaOpenRunAgent(workItemId, agentId) {
|
|
|
504
504
|
loadQaTargets();
|
|
505
505
|
loadQaRunbooks();
|
|
506
506
|
loadQaRunners();
|
|
507
|
+
loadQaProjectsSelect();
|
|
507
508
|
loadQaSessions();
|
|
508
509
|
_startQaSessionsPoll();
|
|
509
510
|
_startQaRunsPoll();
|
|
@@ -594,6 +595,38 @@ async function loadQaRunners() {
|
|
|
594
595
|
}
|
|
595
596
|
}
|
|
596
597
|
|
|
598
|
+
// W-mpq6xqzj000606d0 — Populate the multi-select projects dropdown on the
|
|
599
|
+
// QA Session form. Prefers the in-memory `cmdProjects` cache (populated by
|
|
600
|
+
// /api/status renders), falling back to a direct /api/status fetch when the
|
|
601
|
+
// QA page is opened in isolation before any other page has loaded.
|
|
602
|
+
async function loadQaProjectsSelect() {
|
|
603
|
+
const sel = document.getElementById('qa-session-projects');
|
|
604
|
+
if (!sel) return;
|
|
605
|
+
let projects = [];
|
|
606
|
+
try {
|
|
607
|
+
if (typeof cmdProjects !== 'undefined' && Array.isArray(cmdProjects) && cmdProjects.length > 0) {
|
|
608
|
+
projects = cmdProjects.slice();
|
|
609
|
+
} else {
|
|
610
|
+
const res = await fetch('/api/status');
|
|
611
|
+
const json = res.ok ? await res.json() : {};
|
|
612
|
+
if (Array.isArray(json && json.projects)) {
|
|
613
|
+
projects = json.projects.map(p => ({ name: p.name, description: p.description || '' }));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} catch { projects = []; }
|
|
617
|
+
// Preserve current selection across re-renders.
|
|
618
|
+
const previouslySelected = new Set(Array.from(sel.selectedOptions || []).map(o => o.value));
|
|
619
|
+
while (sel.firstChild) sel.removeChild(sel.firstChild);
|
|
620
|
+
for (const p of projects) {
|
|
621
|
+
if (!p || !p.name) continue;
|
|
622
|
+
const opt = document.createElement('option');
|
|
623
|
+
opt.value = p.name;
|
|
624
|
+
opt.textContent = p.name;
|
|
625
|
+
if (previouslySelected.has(p.name)) opt.selected = true;
|
|
626
|
+
sel.appendChild(opt);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
597
630
|
async function loadQaSessions() {
|
|
598
631
|
const root = document.getElementById('qa-sessions-content');
|
|
599
632
|
if (!root) return;
|
|
@@ -773,7 +806,13 @@ async function qaSubmitSessionForm() {
|
|
|
773
806
|
const flowsRaw = (document.getElementById('qa-session-flows') || {}).value || '';
|
|
774
807
|
const mode = (document.getElementById('qa-session-mode') || {}).value || 'confirm';
|
|
775
808
|
const runner = (document.getElementById('qa-session-runner') || {}).value || '';
|
|
776
|
-
|
|
809
|
+
// W-mpq6xqzj000606d0 — Multi-select projects dropdown. First selected =
|
|
810
|
+
// primary (drives DRAFT/EXECUTE); rest = co-services (dev-up only).
|
|
811
|
+
// Empty selection = central (no project).
|
|
812
|
+
const projectsSel = document.getElementById('qa-session-projects');
|
|
813
|
+
const projects = projectsSel
|
|
814
|
+
? Array.from(projectsSel.selectedOptions || []).map(o => o.value).filter(Boolean)
|
|
815
|
+
: [];
|
|
777
816
|
const capture = {
|
|
778
817
|
video: !!(document.getElementById('qa-session-capture-video') || {}).checked,
|
|
779
818
|
screenshots: !!(document.getElementById('qa-session-capture-screenshots') || {}).checked,
|
|
@@ -781,7 +820,7 @@ async function qaSubmitSessionForm() {
|
|
|
781
820
|
};
|
|
782
821
|
const body = { target, flowsRaw, mode, capture };
|
|
783
822
|
if (runner) body.runner = runner;
|
|
784
|
-
if (
|
|
823
|
+
if (projects.length > 0) body.projects = projects;
|
|
785
824
|
|
|
786
825
|
try {
|
|
787
826
|
const res = await fetch('/api/qa/session', {
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -104,6 +104,22 @@ const _sectionCacheVersions = {};
|
|
|
104
104
|
// into the ring-buffer entry then resets _lastChangedFlags to null so steady-
|
|
105
105
|
// state has no side effect. See "Refresh diagnostics" block below.
|
|
106
106
|
let _lastChangedFlags = null;
|
|
107
|
+
|
|
108
|
+
// Per-renderer isolation: a bare `renderX(...)` call that throws used to
|
|
109
|
+
// abort the rest of _processStatusUpdate, leaving the work-items and
|
|
110
|
+
// dispatch tables frozen until a hard refresh. _safeRender wraps each
|
|
111
|
+
// call so one throw can't take out the chain — every downstream renderer
|
|
112
|
+
// still runs and paints fresh DOM. Throws are logged to Console for
|
|
113
|
+
// triage but don't surface a UI banner (intentional — silent recovery is
|
|
114
|
+
// less disruptive than a red banner for what's typically a transient
|
|
115
|
+
// data-shape blip).
|
|
116
|
+
function _safeRender(name, fn) {
|
|
117
|
+
try { fn(); }
|
|
118
|
+
catch (e) {
|
|
119
|
+
// eslint-disable-next-line no-console
|
|
120
|
+
console.error('[render] ' + name + ' threw:', e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
107
123
|
function _changed(key, value, version) {
|
|
108
124
|
var v = version == null ? (RENDER_VERSIONS[key] || 0) : version;
|
|
109
125
|
// Drop the stale-version entry so the cache doesn't grow unbounded across bumps.
|
|
@@ -294,12 +310,13 @@ function _processStatusUpdate(data) {
|
|
|
294
310
|
window._lastStatus = data;
|
|
295
311
|
|
|
296
312
|
|
|
297
|
-
// Render every section every tick
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
313
|
+
// Render every section every tick, each call ISOLATED via _safeRender so a
|
|
314
|
+
// single bad-data renderer can't abort the rest of the chain. Prior bug:
|
|
315
|
+
// an upstream throw (e.g. renderEngineStatus on a partial engine slice)
|
|
316
|
+
// propagated to refresh()'s outer catch, skipping every downstream
|
|
317
|
+
// renderer — including renderWorkItems below — until the offending record
|
|
318
|
+
// cycled out of the slim /api/status payload. Per-call try/catch keeps
|
|
319
|
+
// each renderer independent; throws log to Console (no UI banner).
|
|
303
320
|
//
|
|
304
321
|
// We KEEP the `_changed(...)` CALLS (their side effect of populating
|
|
305
322
|
// `_lastChangedFlags` is still load-bearing for the diag ring-buffer
|
|
@@ -308,80 +325,78 @@ function _processStatusUpdate(data) {
|
|
|
308
325
|
// value to gate the render. Each renderer is a contained DOM rewrite
|
|
309
326
|
// (~ a few KB of HTML); ~10–20 of them per 4s tick is well under one
|
|
310
327
|
// frame's budget.
|
|
311
|
-
//
|
|
312
|
-
// Gates intentionally kept: none on the render path. The render-versions
|
|
313
|
-
// bump path (RENDER_VERSIONS map + _changed's stringify cache) is still
|
|
314
|
-
// useful for the diag flag, just not as a render skip.
|
|
315
328
|
_changed('agents', data.agents);
|
|
316
|
-
renderAgents(data.agents);
|
|
317
|
-
cmdUpdateAgentList(data.agents);
|
|
329
|
+
_safeRender('agents', function() { renderAgents(data.agents); });
|
|
330
|
+
_safeRender('cmdUpdateAgentList', function() { cmdUpdateAgentList(data.agents); });
|
|
318
331
|
// prdProgress + prdPrs are captured together so both flags publish to
|
|
319
332
|
// the diag buffer; the renderer + cachePrdItems run unconditionally.
|
|
320
333
|
_changed('prdProgress', data.prdProgress);
|
|
321
334
|
_changed('prdPrs', data.pullRequests?.length);
|
|
322
|
-
renderPrdProgress(data.prdProgress);
|
|
323
|
-
_cachePrdItems(data.prdProgress);
|
|
335
|
+
_safeRender('prdProgress', function() { renderPrdProgress(data.prdProgress); });
|
|
336
|
+
_safeRender('cachePrdItems', function() { _cachePrdItems(data.prdProgress); });
|
|
324
337
|
_changed('inbox', data.inbox);
|
|
325
|
-
renderInbox(data.inbox || []);
|
|
338
|
+
_safeRender('inbox', function() { renderInbox(data.inbox || []); });
|
|
326
339
|
_changed('projects', data.projects);
|
|
327
|
-
cmdUpdateProjectList(data.projects || []);
|
|
328
|
-
renderProjects(data.projects || []);
|
|
340
|
+
_safeRender('cmdUpdateProjectList', function() { cmdUpdateProjectList(data.projects || []); });
|
|
341
|
+
_safeRender('projects', function() { renderProjects(data.projects || []); });
|
|
329
342
|
// FRE banner — safe to call every tick (idempotent + cheap). Pass the full
|
|
330
343
|
// status payload so the runtime-CLI explainer reads autoMode.defaultCli from
|
|
331
344
|
// THIS tick (window._lastStatus is hoisted above, but renderFre takes the
|
|
332
345
|
// payload directly to avoid the window-global indirection).
|
|
333
346
|
if (typeof renderFre === 'function') {
|
|
334
|
-
|
|
347
|
+
_safeRender('fre', function() { renderFre(data); });
|
|
335
348
|
}
|
|
336
349
|
_changed('notes', data.notes);
|
|
337
|
-
renderNotes(data.notes);
|
|
350
|
+
_safeRender('notes', function() { renderNotes(data.notes); });
|
|
338
351
|
_changed('prd', [data.prd, data.prdProgress]);
|
|
339
|
-
renderPrd(data.prd, data.prdProgress);
|
|
352
|
+
_safeRender('prd', function() { renderPrd(data.prd, data.prdProgress); });
|
|
340
353
|
// Capture prs + workItems change signals once — also reused by the cross-slice
|
|
341
354
|
// render triggers at the bottom of this function (F1/F3, W-mpgb0xbh000e3b86).
|
|
342
355
|
// _changed mutates _sectionCache so it must be called exactly once per key.
|
|
343
356
|
var _prsChanged = _changed('prs', data.pullRequests);
|
|
344
357
|
var _workItemsChanged = _changed('workItems', data.workItems);
|
|
345
|
-
renderPrs(data.pullRequests || []);
|
|
358
|
+
_safeRender('prs', function() { renderPrs(data.pullRequests || []); });
|
|
346
359
|
_changed('archivedPrds', data.archivedPrds);
|
|
347
|
-
renderArchiveButtons(data.archivedPrds || []);
|
|
360
|
+
_safeRender('archiveButtons', function() { renderArchiveButtons(data.archivedPrds || []); });
|
|
348
361
|
_changed('engine', data.engine);
|
|
349
|
-
if (data.engine) renderEngineStatus(data.engine);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
'<span>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
362
|
+
if (data.engine) _safeRender('engineStatus', function() { renderEngineStatus(data.engine); });
|
|
363
|
+
_safeRender('engineQuickStats', function() {
|
|
364
|
+
var qs = document.getElementById('engine-quick-stats');
|
|
365
|
+
if (qs && data.engine) {
|
|
366
|
+
var wt = data.engine.worktreeCount != null ? data.engine.worktreeCount : '-';
|
|
367
|
+
var pid = data.engine.pid || '-';
|
|
368
|
+
// W-mpnc4u8c001d9d6c — replace the dead "Tick: -" chip (control.json
|
|
369
|
+
// never carried a `tick` field) with a live "Next tick in Xs" countdown
|
|
370
|
+
// driven by engine.lastTickAt (stamped at the start of every tickInner)
|
|
371
|
+
// and engine.tickInterval (config, surfaced in the status payload).
|
|
372
|
+
// _updateNextTickChip below ticks the inner span every 1s without
|
|
373
|
+
// re-rendering this whole row.
|
|
374
|
+
_engineCountdown.lastTickAt = Number(data.engine.lastTickAt) || 0;
|
|
375
|
+
_engineCountdown.tickInterval = Number(data.engine.tickInterval) || 0;
|
|
376
|
+
_engineCountdown.engineState = data.engine.state || 'stopped';
|
|
377
|
+
// Feed the cadence ring buffer so the overshoot label can surface the
|
|
378
|
+
// observed tick-to-tick gap (W-mpodheao0006a37a).
|
|
379
|
+
_recordEngineTickObservation(_engineCountdown.lastTickAt);
|
|
380
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal engine metrics (pid, lastTickAt/tickInterval, worktreeCount) and a literal id; no user data flows in
|
|
381
|
+
qs.innerHTML = '<span>PID: <b>' + pid + '</b></span>' +
|
|
382
|
+
'<span>Next tick in: <b id="engine-next-tick">' + _formatNextTickText() + '</b></span>' +
|
|
383
|
+
'<span>Worktrees: <b>' + wt + '</b></span>';
|
|
384
|
+
_startNextTickTicker();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
372
387
|
_changed('version', data.version);
|
|
373
|
-
renderVersionBanner(data.version);
|
|
388
|
+
_safeRender('versionBanner', function() { renderVersionBanner(data.version); });
|
|
374
389
|
_changed('adoThrottle', data.adoThrottle);
|
|
375
|
-
renderAdoThrottleAlert(data.adoThrottle);
|
|
390
|
+
_safeRender('adoThrottle', function() { renderAdoThrottleAlert(data.adoThrottle); });
|
|
376
391
|
_changed('ghThrottle', data.ghThrottle);
|
|
377
|
-
renderGhThrottleAlert(data.ghThrottle);
|
|
392
|
+
_safeRender('ghThrottle', function() { renderGhThrottleAlert(data.ghThrottle); });
|
|
378
393
|
_changed('dispatch', data.dispatch);
|
|
379
|
-
renderDispatch(data.dispatch);
|
|
380
|
-
prunePrdRequeueState(window._lastWorkItems);
|
|
394
|
+
_safeRender('dispatch', function() { renderDispatch(data.dispatch); });
|
|
395
|
+
_safeRender('prunePrdRequeueState', function() { prunePrdRequeueState(window._lastWorkItems); });
|
|
381
396
|
_changed('engineLog', data.engineLog);
|
|
382
|
-
renderEngineLog(data.engineLog || []);
|
|
397
|
+
_safeRender('engineLog', function() { renderEngineLog(data.engineLog || []); });
|
|
383
398
|
_changed('metrics', data.metrics);
|
|
384
|
-
renderMetrics(data.metrics || {});
|
|
399
|
+
_safeRender('metrics', function() { renderMetrics(data.metrics || {}); });
|
|
385
400
|
// managed-processes panel — ETag-gated so unchanged ticks return 304 with
|
|
386
401
|
// no body (P-6e2a8b13). Sequenced BEFORE the keep-processes call below via
|
|
387
402
|
// .then() so the keep renderer reads a populated managed-PID cache for
|
|
@@ -400,21 +415,23 @@ function _processStatusUpdate(data) {
|
|
|
400
415
|
.catch(function () { /* keep render even if managed fetch failed — getLastItems() returns the last good cache (or []) */ })
|
|
401
416
|
.then(function () { try { renderKeepProcesses(); } catch {} });
|
|
402
417
|
}
|
|
403
|
-
renderWorkItems(data.workItems || []);
|
|
418
|
+
_safeRender('workItems', function() { renderWorkItems(data.workItems || []); });
|
|
404
419
|
_changed('skills', data.skills);
|
|
405
|
-
renderSkills(data.skills || []);
|
|
420
|
+
_safeRender('skills', function() { renderSkills(data.skills || []); });
|
|
406
421
|
_changed('mcpServers', data.mcpServers);
|
|
407
|
-
renderMcpServers(data.mcpServers || []);
|
|
422
|
+
_safeRender('mcpServers', function() { renderMcpServers(data.mcpServers || []); });
|
|
408
423
|
_changed('schedules', data.schedules);
|
|
409
|
-
renderSchedules(data.schedules || []);
|
|
424
|
+
_safeRender('schedules', function() { renderSchedules(data.schedules || []); });
|
|
410
425
|
_changed('watches', data.watches);
|
|
411
|
-
renderWatches(data.watches || []);
|
|
426
|
+
_safeRender('watches', function() { renderWatches(data.watches || []); });
|
|
412
427
|
_changed('meetings', data.meetings);
|
|
413
|
-
renderMeetings(data.meetings || []);
|
|
428
|
+
_safeRender('meetings', function() { renderMeetings(data.meetings || []); });
|
|
414
429
|
_changed('pipelines', data.pipelines);
|
|
415
|
-
if (typeof renderPipelines === 'function')
|
|
430
|
+
if (typeof renderPipelines === 'function') {
|
|
431
|
+
_safeRender('pipelines', function() { renderPipelines(data.pipelines || []); });
|
|
432
|
+
}
|
|
416
433
|
_changed('pinned', data.pinned);
|
|
417
|
-
renderPinned(data.pinned || []);
|
|
434
|
+
_safeRender('pinned', function() { renderPinned(data.pinned || []); });
|
|
418
435
|
// Sidebar counts (cheap)
|
|
419
436
|
const swi = document.getElementById('sidebar-wi');
|
|
420
437
|
if (swi) swi.textContent = (data.workItems || []).length || '';
|
|
@@ -430,8 +447,8 @@ function _processStatusUpdate(data) {
|
|
|
430
447
|
// and after every kb-sweep (engine/queries.js _kbCache / kb-sweep.js).
|
|
431
448
|
// Previously throttled to every 3rd cycle (~12s) — see W-mphfb6ss000a3b9e
|
|
432
449
|
// for the cadence audit + Playwright coverage.
|
|
433
|
-
refreshKnowledgeBase();
|
|
434
|
-
refreshPlans();
|
|
450
|
+
_safeRender('refreshKnowledgeBase', function() { refreshKnowledgeBase(); });
|
|
451
|
+
_safeRender('refreshPlans', function() { refreshPlans(); });
|
|
435
452
|
|
|
436
453
|
// Cross-slice render triggers (F1/F3, W-mpgb0xbh000e3b86): renderPrs reads
|
|
437
454
|
// window._lastWorkItems for the +N follow-up chip count and derivePlanStatus
|
|
@@ -444,14 +461,14 @@ function _processStatusUpdate(data) {
|
|
|
444
461
|
// F1: only the work-item slice moved this tick — renderPrs wasn't called
|
|
445
462
|
// above, so the +N follow-up chip would otherwise stay stale until the
|
|
446
463
|
// next PR mutation.
|
|
447
|
-
renderPrs(data.pullRequests || []);
|
|
464
|
+
_safeRender('prs:cross-slice', function() { renderPrs(data.pullRequests || []); });
|
|
448
465
|
}
|
|
449
466
|
if ((_workItemsChanged || _prsChanged) && Array.isArray(window._lastPlans) && typeof renderPlans === 'function') {
|
|
450
467
|
// F3: derivePlanStatus + _renderVerifyBadge derive from pullRequests +
|
|
451
468
|
// workItems. Re-render against cached plans so plan status flips within
|
|
452
469
|
// one /api/status tick (~4s) instead of one refreshPlans poll. No-op
|
|
453
470
|
// until _lastPlans is populated.
|
|
454
|
-
renderPlans(window._lastPlans);
|
|
471
|
+
_safeRender('plans:cross-slice', function() { renderPlans(window._lastPlans); });
|
|
455
472
|
}
|
|
456
473
|
|
|
457
474
|
// Sidebar activity indicators — show red dot on pages with new activity
|