@beastmode-develeap/beastmode 0.1.301 → 0.1.302

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.
@@ -15,7 +15,7 @@
15
15
  }
16
16
  </script>
17
17
  <!--BOARD_DATA-->
18
- <script>window.__BUILD_STAMP__ = "20260519-160800-f4fed5f";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260525-101559-660ede4";</script>
19
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -11643,6 +11643,204 @@ function LearningsPage({ selectedProject }) {
11643
11643
  `;
11644
11644
  }
11645
11645
 
11646
+ // ================================================================
11647
+ // Investigator Status Bar + Activity Drawer (Story S2, item #660)
11648
+ // ================================================================
11649
+
11650
+ // Explicit terminal-state status selector for the investigator status bar.
11651
+ // Blocked and Escalated are STICKY — they win regardless of timestamps and can
11652
+ // only be displaced by a newer InvestigatorResolved. System-scan events
11653
+ // (InvestigatorObserved for item #0 / null) are ignored entirely.
11654
+ // Does NOT use Array.reduce() so there is no risk of a newer scan timestamp
11655
+ // silently winning a comparator-based fold.
11656
+ function _computeInvestigatorStatus(events) {
11657
+ if (!events || events.length === 0) return null;
11658
+ let bestTerminal = null; // most-recent Blocked or Escalated
11659
+ let bestActivity = null; // most-recent non-terminal, non-scan event
11660
+ for (const e of events) {
11661
+ const ts = e.timestamp || '';
11662
+ const isTerminal = e.type === 'InvestigatorBlocked' || e.type === 'InvestigatorEscalated';
11663
+ const isResolution = e.type === 'InvestigatorResolved';
11664
+ const isSystemScan = !e.item_id || e.item_id === '0' || e.item_id === 0
11665
+ || (e.payload && e.payload.summary === 'Investigator system scan');
11666
+ if (isTerminal) {
11667
+ if (!bestTerminal || ts > (bestTerminal.timestamp || '')) bestTerminal = e;
11668
+ } else if (isResolution) {
11669
+ if (bestTerminal && ts > (bestTerminal.timestamp || '')) bestTerminal = null;
11670
+ if (!bestActivity || ts > (bestActivity.timestamp || '')) bestActivity = e;
11671
+ } else if (!isSystemScan) {
11672
+ if (!bestActivity || ts > (bestActivity.timestamp || '')) bestActivity = e;
11673
+ }
11674
+ }
11675
+ return bestTerminal || bestActivity;
11676
+ }
11677
+
11678
+ function InvestigatorStatusBar({ events, onOpenDrawer, drawerOpen }) {
11679
+ // events is pre-filtered (system scans excluded at accumulation time in App).
11680
+ // _computeInvestigatorStatus adds a second layer of defence.
11681
+ const latest = _computeInvestigatorStatus(events);
11682
+
11683
+ let text = 'Investigator: idle';
11684
+ let dotColor = 'var(--text-muted)';
11685
+ let textColor = 'var(--text)'; // --text-muted fails 3.0 WCAG ratio on --bg-card in light theme
11686
+
11687
+ if (latest) {
11688
+ const ts = latest.timestamp ? new Date(latest.timestamp).getTime() : 0;
11689
+ const ageMs = Date.now() - ts;
11690
+ const tooOld = !ts || ageMs > 600000;
11691
+ const item = latest.item_id || '';
11692
+ const resolvedFading = latest.type === 'InvestigatorResolved' && ageMs > 60000;
11693
+ if (tooOld || resolvedFading) {
11694
+ text = 'Investigator: idle';
11695
+ } else if (latest.type === 'InvestigatorObserved') {
11696
+ text = item ? `Investigator: observing #${item}` : 'Investigator: observing (system scan)';
11697
+ dotColor = 'var(--accent)';
11698
+ textColor = 'var(--text)';
11699
+ } else if (latest.type === 'InvestigatorActed' || latest.type === 'InvestigatorHypothesized') {
11700
+ text = `Investigator: acting on #${item}`;
11701
+ dotColor = 'var(--accent)';
11702
+ textColor = 'var(--text)';
11703
+ } else if (latest.type === 'InvestigatorEscalated') {
11704
+ text = `Investigator: escalated on #${item}`;
11705
+ dotColor = 'var(--danger)';
11706
+ textColor = 'var(--danger)';
11707
+ } else if (latest.type === 'InvestigatorBlocked') {
11708
+ const isHold = latest && latest.payload && latest.payload.reason === 'operator_hold';
11709
+ text = isHold ? `Investigator: held by operator on #${item}` : `Investigator: blocked on #${item}`;
11710
+ dotColor = 'var(--warning)';
11711
+ textColor = 'var(--warning)';
11712
+ } else if (latest.type === 'InvestigatorResolved') {
11713
+ text = `Investigator: resolved #${item}`;
11714
+ dotColor = 'var(--success)';
11715
+ textColor = 'var(--success)';
11716
+ }
11717
+ }
11718
+
11719
+ return html`
11720
+ <button
11721
+ data-testid="investigator-status"
11722
+ aria-label="Open investigator activity drawer"
11723
+ aria-expanded=${drawerOpen ? 'true' : 'false'}
11724
+ onClick=${onOpenDrawer}
11725
+ style="display:flex;align-items:center;gap:8px;padding:6px 10px;margin-bottom:12px;font-size:12px;color:${textColor};background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-family:inherit;">
11726
+ <span style="width:8px;height:8px;border-radius:50%;background:${dotColor};display:inline-block;" />
11727
+ ${text}
11728
+ </button>
11729
+ `;
11730
+ }
11731
+
11732
+ function InvestigatorActivityDrawer({ open, onClose, events, severity, onSeverityChange }) {
11733
+ const order = { debug: 0, info: 1, notice: 2, warn: 3 };
11734
+ const threshold = severity === 'all' ? 0 : severity === 'info+' ? 1 : 2;
11735
+ const now = Date.now();
11736
+
11737
+ const filtered = (events || [])
11738
+ .filter(e => {
11739
+ const ts = e && e.timestamp ? new Date(e.timestamp).getTime() : 0;
11740
+ if (!ts || now - ts > 600000) return false;
11741
+ const raw = e && e.payload ? e.payload.severity : undefined;
11742
+ const lvl = (raw in order) ? order[raw] : 2;
11743
+ return lvl >= threshold;
11744
+ })
11745
+ .sort((a, b) => {
11746
+ const ta = a.timestamp || '';
11747
+ const tb = b.timestamp || '';
11748
+ if (ta !== tb) return ta < tb ? 1 : -1;
11749
+ // Same second: higher id comes first (later-inserted = newer)
11750
+ return (parseInt(b.id) || 0) - (parseInt(a.id) || 0);
11751
+ });
11752
+
11753
+ const [expanded, setExpanded] = useState({});
11754
+
11755
+ return html`
11756
+ ${open ? html`
11757
+ <div
11758
+ data-testid="investigator-drawer-backdrop"
11759
+ onClick=${onClose}
11760
+ style="position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:199;" />
11761
+ ` : null}
11762
+ <aside
11763
+ data-testid="investigator-drawer"
11764
+ style="position:fixed;top:0;right:0;height:100vh;width:min(480px,90vw);background:var(--bg-card);border-left:1px solid var(--border);box-shadow:var(--shadow-sm);transform:translateX(${open ? '0' : '100%'});transition:transform 200ms ease-out;z-index:200;display:flex;flex-direction:column;">
11765
+ <header style="padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;">
11766
+ <strong style="flex:1;color:var(--text);">Investigator activity</strong>
11767
+ <select
11768
+ data-testid="investigator-severity-filter"
11769
+ value=${severity}
11770
+ onChange=${(e) => onSeverityChange(e.target.value)}
11771
+ style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--radius-sm);padding:2px 6px;">
11772
+ <option value="notice+">notice+</option>
11773
+ <option value="info+">info+</option>
11774
+ <option value="all">all</option>
11775
+ </select>
11776
+ <button
11777
+ data-testid="investigator-drawer-close"
11778
+ aria-label="Close drawer"
11779
+ onClick=${onClose}
11780
+ style="background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;">${'×'}</button>
11781
+ </header>
11782
+ <div style="flex:1;overflow-y:auto;padding:8px 0;">
11783
+ ${filtered.length === 0 ? html`
11784
+ <div
11785
+ data-testid="investigator-empty-state"
11786
+ style="padding:24px 16px;color:var(--text);font-size:13px;text-align:center;">
11787
+ No investigator activity in the last 10 minutes.
11788
+ </div>
11789
+ ` : filtered.map(e => {
11790
+ const isExpanded = !!expanded[e.id];
11791
+ const shortType = (e.type || '').replace(/^Investigator/, '');
11792
+ const ts = (e.timestamp || '').slice(11, 19);
11793
+ const summary = e && e.payload && e.payload.summary ? e.payload.summary : '';
11794
+ return html`
11795
+ <div
11796
+ data-testid="investigator-event-row"
11797
+ data-event-id=${e.id}
11798
+ data-event-type=${e.type}
11799
+ style="padding:8px 16px;border-bottom:1px solid var(--border);font-size:12px;color:var(--text);">
11800
+ <div style="display:flex;align-items:center;gap:4px;">
11801
+ <span style="color:var(--text-muted);font-variant-numeric:tabular-nums;">${ts}</span>
11802
+ <span style="color:var(--text-muted);" aria-hidden="true"> · </span>
11803
+ <span style="font-weight:600;">${shortType}</span>
11804
+ <span style="color:var(--text-muted);" aria-hidden="true"> · </span>
11805
+ ${e.item_id
11806
+ ? html`<a href="#/item/${e.item_id}" style="color:var(--accent);text-decoration:none;">#${e.item_id}</a>`
11807
+ : html`<span style="color:var(--text-muted);">system</span>`}
11808
+ <span style="color:var(--text-muted);" aria-hidden="true"> · </span>
11809
+ <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);">${summary}</span>
11810
+ ${e && e.payload && e.payload.details && e.payload.details.kb_hit ? html`
11811
+ <span
11812
+ data-testid="investigator-kb-badge"
11813
+ data-kb-signature=${(e.payload.details.kb_signature) || ''}
11814
+ style="flex-shrink:0;font-size:11px;padding:2px 6px;border-radius:var(--radius-sm);background:var(--accent);color:var(--bg);font-weight:600;white-space:nowrap;">
11815
+ known failure shape, fixed ${e.payload.details.kb_success_count || 0} times
11816
+ </span>
11817
+ ` : null}
11818
+ <button
11819
+ data-testid="investigator-event-expand"
11820
+ aria-expanded=${isExpanded ? 'true' : 'false'}
11821
+ onClick=${() => setExpanded(prev => ({ ...prev, [e.id]: !prev[e.id] }))}
11822
+ style="flex-shrink:0;background:transparent;border:none;color:var(--text-muted);cursor:pointer;padding:0 4px;">
11823
+ ${isExpanded ? '▾' : '▸'}
11824
+ </button>
11825
+ </div>
11826
+ ${isExpanded ? html`
11827
+ <pre
11828
+ data-testid="investigator-event-details"
11829
+ style="margin:8px 0 0 0;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);font-size:11px;overflow-x:auto;">${`type: ${e.type}\n${JSON.stringify(e.payload, null, 2)}`}</pre>
11830
+ ` : null}
11831
+ </div>
11832
+ `;
11833
+ })}
11834
+ </div>
11835
+ </aside>
11836
+ `;
11837
+ }
11838
+ // Expose so verifiers and future patches can assert exactly one live reference
11839
+ try { window.__InvestigatorActivityDrawer = InvestigatorActivityDrawer; } catch (_) {}
11840
+
11841
+ // Expose on window so the verifier can inspect/test without navigating component trees.
11842
+ try { window.__InvestigatorActivityDrawer = InvestigatorActivityDrawer; } catch (_) {}
11843
+
11646
11844
  // ================================================================
11647
11845
  // Factory Status Bar
11648
11846
  // ================================================================
@@ -11954,6 +12152,67 @@ function App() {
11954
12152
  setMobileMenuOpen(false);
11955
12153
  }, [hash]);
11956
12154
 
12155
+ // Investigator status indicator + activity drawer (Story S2, item #660).
12156
+ const [investigatorDrawerOpen, setInvestigatorDrawerOpen] = useState(false);
12157
+ // All investigator events — fed to the activity drawer.
12158
+ const [investigatorEvents, setInvestigatorEvents] = useState([]);
12159
+ // Non-scan investigator events only — fed to the status bar.
12160
+ // System-scan events (InvestigatorObserved for item #0) are excluded here so
12161
+ // they can never appear in the status bar's input even before _computeInvestigatorStatus runs.
12162
+ const [investigatorStatusEvents, setInvestigatorStatusEvents] = useState([]);
12163
+ const [investigatorSeverity, setInvestigatorSeverity] = useState('notice+');
12164
+
12165
+ useEffect(() => {
12166
+ let since = new Date(Date.now() - 600000).toISOString();
12167
+ const fetchEvents = async () => {
12168
+ try {
12169
+ // Same precedence as api() lines 4004-4015 and ItemDetailSidebar line 5649:
12170
+ // URL ?board= first, then localStorage (excluding the literal 'all'),
12171
+ // else no scope (default board). Without this scoping the UI showed
12172
+ // events from the project board while the verifier was scoped to a
12173
+ // verify-board (bug #698 blocking #660).
12174
+ let board = '';
12175
+ try {
12176
+ board = new URLSearchParams(window.location.search).get('board') || '';
12177
+ if (!board) {
12178
+ const ls = localStorage.getItem('beastmode-selected-project') || '';
12179
+ board = (ls && ls !== 'all') ? ls : '';
12180
+ }
12181
+ } catch (_) { board = ''; }
12182
+ const boardParam = board ? `&board=${encodeURIComponent(board)}` : '';
12183
+ const rows = await api('GET', `/api/events?since=${encodeURIComponent(since)}&limit=500${boardParam}`);
12184
+ const incoming = (rows || []).filter(e => typeof (e && e.type) === 'string' && e.type.startsWith('Investigator'));
12185
+ if (incoming.length > 0) {
12186
+ const latest = incoming.reduce((a, b) => ((a.timestamp || '') > (b.timestamp || '') ? a : b));
12187
+ if (latest.timestamp) since = latest.timestamp;
12188
+ // All events → drawer
12189
+ setInvestigatorEvents(prev => {
12190
+ const seen = new Set(prev.map(e => e.id));
12191
+ const novel = incoming.filter(e => !seen.has(e.id));
12192
+ return novel.length ? [...prev, ...novel] : prev;
12193
+ });
12194
+ // Non-scan events only → status bar (system scan = InvestigatorObserved for item #0)
12195
+ const statusIncoming = incoming.filter(e => {
12196
+ if (!e || e.type !== 'InvestigatorObserved') return true;
12197
+ if (!e.item_id || e.item_id === '0' || e.item_id === 0) return false;
12198
+ if (e.payload && e.payload.summary === 'Investigator system scan') return false;
12199
+ return true;
12200
+ });
12201
+ if (statusIncoming.length > 0) {
12202
+ setInvestigatorStatusEvents(prev => {
12203
+ const seen = new Set(prev.map(e => e.id));
12204
+ const novel = statusIncoming.filter(e => !seen.has(e.id));
12205
+ return novel.length ? [...prev, ...novel] : prev;
12206
+ });
12207
+ }
12208
+ }
12209
+ } catch (_) { /* fail-open: keep prior state */ }
12210
+ };
12211
+ fetchEvents();
12212
+ const id = setInterval(fetchEvents, 3000);
12213
+ return () => clearInterval(id);
12214
+ }, []);
12215
+
11957
12216
  return html`
11958
12217
  <div class="layout">
11959
12218
  <button class="mobile-menu-btn" onClick=${() => setMobileMenuOpen(!mobileMenuOpen)} aria-label="Menu">
@@ -11965,8 +12224,18 @@ function App() {
11965
12224
  mobileOpen=${mobileMenuOpen} />
11966
12225
  <main class="main">
11967
12226
  <${FactoryStatusBar} />
12227
+ <${InvestigatorStatusBar}
12228
+ events=${investigatorStatusEvents}
12229
+ onOpenDrawer=${() => setInvestigatorDrawerOpen(true)}
12230
+ drawerOpen=${investigatorDrawerOpen} />
11968
12231
  ${page}
11969
12232
  </main>
12233
+ <${InvestigatorActivityDrawer}
12234
+ open=${investigatorDrawerOpen}
12235
+ onClose=${() => setInvestigatorDrawerOpen(false)}
12236
+ events=${investigatorEvents}
12237
+ severity=${investigatorSeverity}
12238
+ onSeverityChange=${setInvestigatorSeverity} />
11970
12239
  <${OnboardingTour} />
11971
12240
  </div>
11972
12241
  `;
@@ -1 +1 @@
1
- f4fed5f75c0aba44ea439890f1a099fabe9ada73
1
+ 660ede40db3cd8674e904cbe496dfdab19e03d3e
@@ -1 +1 @@
1
- 20260519-160800-f4fed5f
1
+ 20260525-101559-660ede4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.301",
3
+ "version": "0.1.302",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {