@yemi33/minions 0.1.2100 → 0.1.2102

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.
@@ -49,4 +49,36 @@ function buildDashboardHtml() {
49
49
  .replace('/* __JS__ */', () => jsHtml);
50
50
  }
51
51
 
52
- module.exports = { buildDashboardHtml };
52
+ // Slim-UX surface (feature flag 'slim-ux'). Same fragment-assembly model as
53
+ // the main dashboard: a shell layout with /* __CSS__ */, <!-- __BODY__ -->,
54
+ // /* __JS__ */ placeholders, filled from dashboard/slim/. The JS is one shared
55
+ // IIFE scope, so the parts are concatenated in dependency order (matching the
56
+ // original single-file source) inside the wrapper that layout.html provides.
57
+ const SLIM_JS_ORDER = [
58
+ 'helpers', 'settings', 'link-pr', 'chat', 'projects',
59
+ 'command-send', 'status', 'members', 'modals-tiles', 'history',
60
+ ];
61
+
62
+ function buildSlimHtml() {
63
+ const slimDir = path.join(MINIONS_DIR, 'dashboard', 'slim');
64
+ const layoutPath = path.join(slimDir, 'layout.html');
65
+ if (!fs.existsSync(layoutPath)) {
66
+ throw new Error(`Slim layout not found: ${layoutPath}. The dashboard/slim/ directory must exist.`);
67
+ }
68
+ const layout = safeRead(layoutPath);
69
+ const css = safeRead(path.join(slimDir, 'styles.css'));
70
+ const body = safeRead(path.join(slimDir, 'body.html'));
71
+ // Join with a newline so a fragment that loses its trailing newline can't
72
+ // glue two statements together (e.g. `}var foo`). Parts share one IIFE
73
+ // scope, so order is load-bearing — see SLIM_JS_ORDER.
74
+ const js = SLIM_JS_ORDER.map(f => safeRead(path.join(slimDir, 'js', f + '.js'))).join('\n');
75
+
76
+ return layout
77
+ .replace('/* __CSS__ */', () => css)
78
+ // Tolerate CRLF drift in layout.html (LF is enforced by .gitattributes, but
79
+ // an exact '\n' match would silently no-op and ship an empty body on CRLF).
80
+ .replace(/<!-- __BODY__ -->\r?\n/, () => body)
81
+ .replace('/* __JS__ */', () => js);
82
+ }
83
+
84
+ module.exports = { buildDashboardHtml, buildSlimHtml, SLIM_JS_ORDER };
package/dashboard.js CHANGED
@@ -9559,23 +9559,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9559
9559
  }
9560
9560
 
9561
9561
  // Slim UX surface for the experimental redesigned dashboard.
9562
- // The HTML lives in dashboard/slim.html so the human can iterate on the
9563
- // markup directly — we read the file from disk on each request (no in-
9564
- // memory cache) so editing it and refreshing the browser shows the change
9565
- // without a server restart. Gating happens in the request dispatcher: this
9566
- // helper is only invoked when features.isFeatureOn('slim-ux', CONFIG) is
9567
- // true at request time, so the flag toggle takes effect with no restart.
9562
+ // The markup/CSS/JS live as fragments under dashboard/slim/ (layout.html +
9563
+ // styles.css + body.html + js/*.js) and are assembled by buildSlimHtml() —
9564
+ // freshly on each request (no in-memory cache) so editing a fragment and
9565
+ // refreshing the browser shows the change without a server restart. Gating
9566
+ // happens in the request dispatcher: this helper is only invoked when
9567
+ // features.isFeatureOn('slim-ux', CONFIG) is true at request time, so the
9568
+ // flag toggle takes effect with no restart.
9568
9569
  async function serveSlimUx(req, res) {
9569
9570
  try {
9570
- const slimPath = path.join(MINIONS_DIR, 'dashboard', 'slim.html');
9571
- const html = fs.readFileSync(slimPath, 'utf8')
9571
+ const { buildSlimHtml } = require('./dashboard-build');
9572
+ const html = buildSlimHtml()
9572
9573
  .replace(/\{\{favicon_emoji\}\}/g, FAVICON_EMOJI)
9573
9574
  .replace(/\{\{title_suffix\}\}/g, TITLE_SUFFIX);
9574
9575
  res.statusCode = 200;
9575
9576
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
9576
9577
  res.setHeader('Cache-Control', 'no-store');
9577
- // slim.html ships an inline <script> IIFE for the chatbox + settings dialog
9578
- // and a data: SVG favicon. Override the baseline strict CSP from
9578
+ // The assembled slim HTML ships an inline <script> IIFE for the chatbox +
9579
+ // settings dialog and a data: SVG favicon. Override the baseline strict CSP from
9579
9580
  // buildSecurityHeaders() (which forbids inline scripts) the same way the
9580
9581
  // full SPA route does — strict CSP still applies to all /api/* responses.
9581
9582
  res.setHeader(
@@ -75,11 +75,11 @@ Each fix cause (build-failure, review-feedback, human-feedback, merge-conflict,
75
75
 
76
76
  A paused cause **suppresses further auto-dispatch for that cause** until cleared. Recovery paths (any one):
77
77
 
78
- 1. **Push a new SHA to the PR branch** — the next poll refreshes `pr.headSha`, the evidence fingerprint shifts, and `clearPrNoOpFixAttempt` runs on the next non-noop completion.
78
+ 1. **Push a new SHA to the PR branch** — the next poll refreshes `pr.headSha`, the evidence fingerprint shifts, and `clearPrNoOpFixAttempt` runs on the next non-noop completion. Since #2979 (head-SHA in BUILD_FAILURE + REVIEW_FEEDBACK fingerprints) and W-mpx44p05000ze8d8 (proactive GC sweep in `pollPrStatus`), a force-push also drops the stale `_noOpFixes[cause]` record on the next poll — the dashboard `_pausedCauses` chip clears without waiting for a fresh fix dispatch.
79
79
  2. **Click the red chip on the dashboard** — confirms, then `POST /api/pull-requests/clear-paused-cause` (`dashboard.js`, issue #2969) validates the cause against `shared.PR_FIX_CAUSE`, locates the PR via `mutatePullRequests` across project/central files, and calls `clearPrNoOpFixAttempt` to wipe the cause record.
80
80
  3. **Direct API call** — `POST /api/pull-requests/clear-paused-cause` with `{ prId, cause }`.
81
81
 
82
- The exported `recordPrNoOpFixAttempt` / `clearPrNoOpFixAttempt` helpers in `engine/lifecycle.js` are the only sanctioned entry points; the dispatch evaluator in `engine/lifecycle.js` (search `_noOpFixes[cause]`) gates re-dispatch on `record.paused === true`.
82
+ The exported `recordPrNoOpFixAttempt` / `clearPrNoOpFixAttempt` / `sweepStalePrNoOpFixes` helpers in `engine/lifecycle.js` are the only sanctioned entry points. The dispatch evaluator gates re-dispatch on `shared.isPrNoOpFixCausePaused(pr, cause)` (which re-validates the fingerprint per call), the dashboard chip surface uses `shared.getPrPausedCauses(pr)` (same gate), and the `pollPrStatus` callback in `engine/ado.js` + `engine/github.js` calls `sweepStalePrNoOpFixes(pr)` each tick to GC records whose fingerprint no longer matches the live PR.
83
83
 
84
84
  ## 5. Fix completes
85
85
 
package/engine/ado.js CHANGED
@@ -24,6 +24,12 @@ function engine() {
24
24
  let _dispatch = null;
25
25
  function dispatchModule() { if (!_dispatch) _dispatch = require('./dispatch'); return _dispatch; }
26
26
 
27
+ // Lazy require for lifecycle module — only needed for the stale-noopfix sweep
28
+ // in pollPrStatus (W-mpx44p05000ze8d8). lifecycle.js lazy-requires ado.js back,
29
+ // so this MUST stay lazy to avoid a circular import at module-load time.
30
+ let _lifecycle = null;
31
+ function lifecycleModule() { if (!_lifecycle) _lifecycle = require('./lifecycle'); return _lifecycle; }
32
+
27
33
  const stripRefsHeads = s => (s || '').replace('refs/heads/', '');
28
34
  const getAdoPrUrl = (project, prNumber) => {
29
35
  if (project.prUrlBase) return `${project.prUrlBase}${prNumber}`;
@@ -1476,6 +1482,21 @@ async function pollPrStatus(config) {
1476
1482
  updated = true;
1477
1483
  }
1478
1484
 
1485
+ // W-mpx44p05000ze8d8 — Proactive GC for stale `_noOpFixes` records. Any
1486
+ // cause whose stored evidence fingerprint no longer matches the live PR
1487
+ // (head SHA shifted, build failure reason changed, new human comment,
1488
+ // review status flipped, etc.) is dropped via clearPrNoOpFixAttempt. The
1489
+ // dispatch gate already ignores these stale records (isPrNoOpFixCausePaused
1490
+ // re-validates the fingerprint), but without this sweep they linger in
1491
+ // pull-requests.json forever — bloating the file and confusing the
1492
+ // dashboard `pr-paused-chip`. Runs AFTER all live-field updates so the
1493
+ // fingerprint comparison uses post-poll PR state.
1494
+ try {
1495
+ if (lifecycleModule().sweepStalePrNoOpFixes(pr)) updated = true;
1496
+ } catch (e) {
1497
+ log('warn', `sweepStalePrNoOpFixes for ${pr.id}: ${e.message}`);
1498
+ }
1499
+
1479
1500
  return updated;
1480
1501
  } catch (err) {
1481
1502
  // Auth errors → mark build status stale so dashboard shows uncertainty
package/engine/github.js CHANGED
@@ -23,6 +23,12 @@ function engine() {
23
23
  let _dispatch = null;
24
24
  function dispatchModule() { if (!_dispatch) _dispatch = require('./dispatch'); return _dispatch; }
25
25
 
26
+ // Lazy require for lifecycle module — only needed for the stale-noopfix sweep
27
+ // in pollPrStatus (W-mpx44p05000ze8d8). lifecycle.js lazy-requires github.js
28
+ // back, so this MUST stay lazy to avoid a circular import at module-load time.
29
+ let _lifecycle = null;
30
+ function lifecycleModule() { if (!_lifecycle) _lifecycle = require('./lifecycle'); return _lifecycle; }
31
+
26
32
  // W-mp5trwh60008386d: test seam so unit tests can mock `gh api` shell-outs
27
33
  // without spawning the real CLI. Production code goes through the real
28
34
  // `execAsync` from shared; tests call `_setExecAsyncForTest(fn)` to swap in
@@ -1021,6 +1027,21 @@ async function pollPrStatus(config) {
1021
1027
  }
1022
1028
  }
1023
1029
 
1030
+ // W-mpx44p05000ze8d8 — Proactive GC for stale `_noOpFixes` records. Any
1031
+ // cause whose stored evidence fingerprint no longer matches the live PR
1032
+ // (head SHA shifted, build failure reason changed, new human comment,
1033
+ // review status flipped, etc.) is dropped via clearPrNoOpFixAttempt. The
1034
+ // dispatch gate already ignores these stale records (isPrNoOpFixCausePaused
1035
+ // re-validates the fingerprint), but without this sweep they linger in
1036
+ // pull-requests.json forever — bloating the file and confusing the
1037
+ // dashboard `pr-paused-chip`. Runs AFTER all live-field updates so the
1038
+ // fingerprint comparison uses post-poll PR state.
1039
+ try {
1040
+ if (lifecycleModule().sweepStalePrNoOpFixes(pr)) updated = true;
1041
+ } catch (e) {
1042
+ log('warn', `sweepStalePrNoOpFixes for ${pr.id}: ${e.message}`);
1043
+ }
1044
+
1024
1045
  return updated;
1025
1046
  });
1026
1047
 
@@ -2090,6 +2090,37 @@ function clearPrNoOpFixAttempt(target, cause) {
2090
2090
  }
2091
2091
  }
2092
2092
 
2093
+ // W-mpx44p05000ze8d8 — Proactive GC for stale `_noOpFixes` records. Called
2094
+ // from `engine/ado.js` + `engine/github.js` `pollPrStatus` after the live PR
2095
+ // fields (headSha, buildStatus, humanFeedback, mergeStatus, reviewStatus,
2096
+ // lastPushedAt, etc.) have been refreshed for the current tick. Any
2097
+ // `_noOpFixes[cause]` whose stored `evidenceFingerprint` no longer matches
2098
+ // the live one is dropped via `clearPrNoOpFixAttempt` — same cleanup the
2099
+ // resume endpoint and successful-fix path already use. Records without a
2100
+ // stored `evidenceFingerprint` (impossible from `recordPrNoOpFixAttempt`
2101
+ // today, but safe against future / legacy data) also drop, because the
2102
+ // dispatch gate already ignores them. Returns true when any record was
2103
+ // removed so the poll caller can flip its `updated` flag to persist the
2104
+ // purge.
2105
+ function sweepStalePrNoOpFixes(target) {
2106
+ if (!target || !target._noOpFixes || typeof target._noOpFixes !== 'object') return false;
2107
+ let modified = false;
2108
+ // Snapshot cause keys before mutation — clearPrNoOpFixAttempt deletes the
2109
+ // whole `_noOpFixes` object once the last record is removed, which would
2110
+ // invalidate a live `Object.keys()` iterator.
2111
+ const causes = Object.keys(target._noOpFixes);
2112
+ for (const cause of causes) {
2113
+ const record = target._noOpFixes[cause];
2114
+ if (!record || typeof record !== 'object') continue;
2115
+ const liveFp = shared.prFixEvidenceFingerprint(target, cause);
2116
+ if (record.evidenceFingerprint !== liveFp) {
2117
+ clearPrNoOpFixAttempt(target, cause);
2118
+ modified = true;
2119
+ }
2120
+ }
2121
+ return modified;
2122
+ }
2123
+
2093
2124
  function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId = '') {
2094
2125
  if (!pr?.id) return;
2095
2126
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
@@ -5137,6 +5168,10 @@ module.exports = {
5137
5168
  // Issue #2969 — exported so dashboard.js can clear a paused cause from the
5138
5169
  // resume endpoint.
5139
5170
  clearPrNoOpFixAttempt,
5171
+ // W-mpx44p05000ze8d8 — exported so engine/ado.js + engine/github.js can
5172
+ // drop stale `_noOpFixes` records during their PR status poll, and so
5173
+ // unit tests can exercise the sweep helper directly.
5174
+ sweepStalePrNoOpFixes,
5140
5175
  // Issue #2969 — exported for direct unit testing of the pause-flip alert
5141
5176
  // path without going through updatePrAfterFix's many guards.
5142
5177
  recordPrNoOpFixAttempt,
package/engine/shared.js CHANGED
@@ -5261,20 +5261,18 @@ function isPrNoOpFixCausePaused(pr, cause) {
5261
5261
  return record.evidenceFingerprint === prFixEvidenceFingerprint(pr, cause);
5262
5262
  }
5263
5263
 
5264
- // Issue #2969 — Derive the list of currently-paused causes for surfacing on
5265
- // `/api/pull-requests` + the dashboard PR card. Returns deduped cause strings
5266
- // from `_noOpFixes[c].paused === true`. Does NOT re-validate evidence
5267
- // fingerprints (the dashboard surface shows the persisted pause flag — when
5268
- // evidence drifts and the discovery filter ignores the stale record, the
5269
- // next fix dispatch overwrites `_noOpFixes[cause]` and naturally re-derives
5270
- // this field on the following enrichment pass).
5264
+ // Issue #2969 / W-mpx44p05000ze8d8 — Derive the list of currently-paused
5265
+ // causes for surfacing on `/api/pull-requests` + the dashboard PR card.
5266
+ // Mirrors `isPrNoOpFixCausePaused` exactly: a cause is "paused" only when
5267
+ // `record.paused === true` AND the stored `evidenceFingerprint` still
5268
+ // matches the live PR's fingerprint. Without the fingerprint check the chip
5269
+ // surfaced stale pauses for PRs the dispatch gate had already cleared (the
5270
+ // engine would re-dispatch but the dashboard kept showing ⏸).
5271
5271
  function getPrPausedCauses(pr) {
5272
5272
  if (!pr || !pr._noOpFixes || typeof pr._noOpFixes !== 'object') return [];
5273
5273
  const causes = [];
5274
- for (const [cause, record] of Object.entries(pr._noOpFixes)) {
5275
- if (record && typeof record === 'object' && record.paused === true) {
5276
- causes.push(cause);
5277
- }
5274
+ for (const cause of Object.keys(pr._noOpFixes)) {
5275
+ if (isPrNoOpFixCausePaused(pr, cause)) causes.push(cause);
5278
5276
  }
5279
5277
  return causes;
5280
5278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2100",
3
+ "version": "0.1.2102",
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"