@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.
@@ -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
- const project = (document.getElementById('qa-session-project') || {}).value || '';
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 (project) body.project = project;
823
+ if (projects.length > 0) body.projects = projects;
785
824
 
786
825
  try {
787
826
  const res = await fetch('/api/qa/session', {
@@ -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 (W-mpn7keq9000302c9 + this commit).
298
- // Every `_changed(...)` GATE was producing the same class of bug
299
- // ref-equality or JSON.stringify short-circuit falsely matching across
300
- // ticks, leaving the DOM frozen until the user did a hard refresh. The
301
- // gates were a small CPU win at the cost of real-time correctness on
302
- // every screen.
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
- try { renderFre(data); } catch { /* expected on first load */ }
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
- var qs = document.getElementById('engine-quick-stats');
351
- if (qs && data.engine) {
352
- var wt = data.engine.worktreeCount != null ? data.engine.worktreeCount : '-';
353
- var pid = data.engine.pid || '-';
354
- // W-mpnc4u8c001d9d6c replace the dead "Tick: -" chip (control.json
355
- // never carried a `tick` field) with a live "Next tick in Xs" countdown
356
- // driven by engine.lastTickAt (stamped at the start of every tickInner)
357
- // and engine.tickInterval (config, surfaced in the status payload).
358
- // _updateNextTickChip below ticks the inner span every 1s without
359
- // re-rendering this whole row.
360
- _engineCountdown.lastTickAt = Number(data.engine.lastTickAt) || 0;
361
- _engineCountdown.tickInterval = Number(data.engine.tickInterval) || 0;
362
- _engineCountdown.engineState = data.engine.state || 'stopped';
363
- // Feed the cadence ring buffer so the overshoot label can surface the
364
- // observed tick-to-tick gap (W-mpodheao0006a37a).
365
- _recordEngineTickObservation(_engineCountdown.lastTickAt);
366
- // 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
367
- qs.innerHTML = '<span>PID: <b>' + pid + '</b></span>' +
368
- '<span>Next tick in: <b id="engine-next-tick">' + _formatNextTickText() + '</b></span>' +
369
- '<span>Worktrees: <b>' + wt + '</b></span>';
370
- _startNextTickTicker();
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') renderPipelines(data.pipelines || []);
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