@axiomatic-labs/claudeflow 2.13.55 → 2.13.57

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/lib/doctor.js CHANGED
@@ -129,6 +129,54 @@ function checkStaleLockfiles(cwd) {
129
129
  };
130
130
  }
131
131
 
132
+ // Read the browser-error-daemon observer state from its pidfile.
133
+ // Returns { running, pid, lockfile } — running=true only when the pid is
134
+ // alive AND the lockfile exists. Used by both doctor and panel.
135
+ function readObserverState(cwd, cdpPort) {
136
+ const lockfile = path.join(cwd, '.claudeflow', 'tmp', `.browser-cdp-${cdpPort}.pid`);
137
+ let pid = null;
138
+ try {
139
+ pid = parseInt(fs.readFileSync(lockfile, 'utf8').trim(), 10);
140
+ } catch {
141
+ return { running: false, pid: null, lockfile };
142
+ }
143
+ if (!Number.isFinite(pid) || pid <= 0) return { running: false, pid, lockfile };
144
+ let alive = false;
145
+ try { process.kill(pid, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
146
+ return { running: alive, pid, lockfile };
147
+ }
148
+
149
+ // Surface as a doctor/panel issue when the browser-error-daemon observer
150
+ // is stopped while Playwright is configured. The observer collects console
151
+ // errors + network failures during headed browser verification — when it's
152
+ // down, those signals are silently lost, so the agent may verify a flow
153
+ // that has console errors and not know it.
154
+ function checkObserverState(cwd) {
155
+ const mcpPath = path.join(cwd, '.mcp.json');
156
+ let cfg;
157
+ try { cfg = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); }
158
+ catch { return { id: 'observer-state', severity: 'info', message: 'No .mcp.json — observer not applicable.' }; }
159
+ const hasPlaywright = cfg.mcpServers
160
+ && Object.keys(cfg.mcpServers).some((s) => s.toLowerCase().includes('playwright'));
161
+ if (!hasPlaywright) {
162
+ return { id: 'observer-state', severity: 'info', message: 'Playwright not configured — observer not applicable.' };
163
+ }
164
+ const observer = readObserverState(cwd, deriveCdpPort(cwd));
165
+ if (observer.running) {
166
+ return {
167
+ id: 'observer-state',
168
+ severity: 'ok',
169
+ message: `browser-error-daemon running (pid ${observer.pid}).`,
170
+ };
171
+ }
172
+ return {
173
+ id: 'observer-state',
174
+ severity: 'mismatch',
175
+ message: 'browser-error-daemon is STOPPED while Playwright is configured. Console errors / network failures from headed sessions are NOT being collected — the agent may verify a flow with console errors and miss them. The observer should auto-start at SessionStart via `.claude/hooks/SessionStart/browser-error-daemon.js`; if it keeps stopping, inspect that hook for crashes or check the pidfile at `.claudeflow/tmp/.browser-cdp-<port>.pid`.',
176
+ detail: [{ lockfile: observer.lockfile, pid: observer.pid }],
177
+ };
178
+ }
179
+
132
180
  function applyStaleLockfileFix(check) {
133
181
  for (const { file } of check.fix.stale) {
134
182
  try { fs.unlinkSync(file); } catch {}
@@ -163,6 +211,7 @@ async function run(argv = []) {
163
211
  const checks = [
164
212
  checkCdpPortMismatch(cwd),
165
213
  checkStaleLockfiles(cwd),
214
+ checkObserverState(cwd),
166
215
  ];
167
216
 
168
217
  for (const check of checks) printCheck(check);
@@ -202,6 +251,8 @@ module.exports = run;
202
251
  module.exports.deriveCdpPort = deriveCdpPort;
203
252
  module.exports.checkCdpPortMismatch = checkCdpPortMismatch;
204
253
  module.exports.checkStaleLockfiles = checkStaleLockfiles;
254
+ module.exports.checkObserverState = checkObserverState;
255
+ module.exports.readObserverState = readObserverState;
205
256
  module.exports.readPlaywrightCdpEndpoint = readPlaywrightCdpEndpoint;
206
257
  module.exports.applyCdpPortFix = applyCdpPortFix;
207
258
  module.exports.applyStaleLockfileFix = applyStaleLockfileFix;
package/lib/panel.js CHANGED
@@ -19,6 +19,8 @@ const {
19
19
  deriveCdpPort,
20
20
  checkCdpPortMismatch,
21
21
  checkStaleLockfiles,
22
+ checkObserverState,
23
+ readObserverState: readObserverStateFromDoctor,
22
24
  readPlaywrightCdpEndpoint,
23
25
  } = require('./doctor.js');
24
26
  const {
@@ -211,19 +213,10 @@ function getMcpInfo(cwd) {
211
213
  };
212
214
  }
213
215
 
214
- function readObserverState(cwd, cdpPort) {
215
- const lockfile = path.join(cwd, '.claudeflow', 'tmp', `.browser-cdp-${cdpPort}.pid`);
216
- let pid = null;
217
- try {
218
- pid = parseInt(fs.readFileSync(lockfile, 'utf8').trim(), 10);
219
- } catch {
220
- return { running: false, pid: null, lockfile };
221
- }
222
- if (!Number.isFinite(pid) || pid <= 0) return { running: false, pid, lockfile };
223
- let alive = false;
224
- try { process.kill(pid, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
225
- return { running: alive, pid, lockfile };
226
- }
216
+ // Re-export the canonical helper from doctor.js so panel callers keep the
217
+ // same name. The implementation moved to doctor.js so both surfaces (CLI
218
+ // doctor + panel) share a single source of truth.
219
+ const readObserverState = readObserverStateFromDoctor;
227
220
 
228
221
  function getSetupContextInfo(cwd) {
229
222
  const p = path.join(cwd, '.claudeflow', 'config', 'setup-context.json');
@@ -524,7 +517,11 @@ function getLogsInfo(cwd) {
524
517
  }
525
518
 
526
519
  function getDoctorInfo(cwd) {
527
- const checks = [checkCdpPortMismatch(cwd), checkStaleLockfiles(cwd)];
520
+ const checks = [
521
+ checkCdpPortMismatch(cwd),
522
+ checkStaleLockfiles(cwd),
523
+ checkObserverState(cwd),
524
+ ];
528
525
  const issues = checks.filter((c) => c.severity !== 'ok' && c.severity !== 'info');
529
526
  return {
530
527
  issueCount: issues.length,
@@ -661,6 +658,9 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
661
658
  .logs-controls { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
662
659
  .logs-controls select, .logs-controls button { background: var(--panel); border: 1px solid var(--border); color: var(--fg); padding: 6px 10px; border-radius: 6px; font: inherit; }
663
660
  .logs-controls button:hover { border-color: var(--accent); cursor: pointer; }
661
+ .btn-warn { background: var(--panel); border: 1px solid rgba(245,194,52,0.5); color: var(--warn, #f5c234); padding: 6px 12px; border-radius: 6px; cursor: pointer; font: inherit; }
662
+ .btn-warn:hover { border-color: var(--warn, #f5c234); background: rgba(245,194,52,0.08); }
663
+ .btn-warn:disabled { opacity: 0.4; cursor: not-allowed; }
664
664
  .logs-clear-btn { margin-left: auto; color: var(--err); border-color: rgba(248,81,73,0.4); }
665
665
  .logs-clear-btn:hover { border-color: var(--err); background: rgba(248,81,73,0.08); }
666
666
  .logs-clear-btn:disabled { opacity: 0.4; cursor: not-allowed; color: var(--muted); border-color: var(--border); }
@@ -872,7 +872,15 @@ function severityFor(id) {
872
872
  if (s.logs.latestReminderStatus === 'heavy') return 'warn';
873
873
  return s.logs.total > 0 ? 'ok' : 'info';
874
874
  }
875
- case 'mcp': return !s.mcp.configFound ? 'info' : (s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0 ? 'ok' : 'warn');
875
+ case 'mcp': {
876
+ if (!s.mcp.configFound) return 'info';
877
+ const playwrightOk = s.mcp.playwright.match && s.mcp.staleLockfiles.length === 0;
878
+ // Observer stopped while Playwright is configured = warn. Console errors
879
+ // and network failures from headed sessions are silently lost when the
880
+ // observer is down.
881
+ const observerOk = !s.mcp.playwright.match || (s.mcp.observer && s.mcp.observer.running);
882
+ return playwrightOk && observerOk ? 'ok' : 'warn';
883
+ }
876
884
  case 'setupContext': return !s.setupContext.exists ? 'err' : (s.setupContext.toolingComplete ? 'ok' : 'warn');
877
885
  case 'activeRun': return s.activeRun.active ? 'info' : 'info';
878
886
  case 'doctor': return s.doctor.issueCount > 0 ? 'warn' : 'ok';
@@ -1294,7 +1302,8 @@ function renderMcp() {
1294
1302
  \${row('Playwright --cdp-endpoint port', String(m.playwright.configured || 'n/a'))}
1295
1303
  \${row('Computed port (from path)', String(m.playwright.computed))}
1296
1304
  \${row('Match', m.playwright.match ? 'YES' : 'NO', { kind: m.playwright.match ? 'ok' : 'err', text: m.playwright.match ? '✓' : '✗' })}
1297
- \${row('Observer', m.observer.running ? 'running (pid '+m.observer.pid+')' : 'stopped', { kind: m.observer.running ? 'ok' : 'info', text: m.observer.running ? '✓' : '·' })}
1305
+ \${row('Observer', m.observer.running ? 'running (pid '+m.observer.pid+')' : 'STOPPED — browser errors NOT being collected', { kind: m.observer.running ? 'ok' : (m.playwright.match ? 'warn' : 'info'), text: m.observer.running ? '✓' : (m.playwright.match ? '⚠' : '·') })}
1306
+ \${!m.observer.running && m.playwright.match ? '<div style="padding:10px 14px;border-top:1px solid var(--border);"><button id="restart-observer" class="btn-warn">Restart observer</button> <span class="muted" style="margin-left:10px;font-size:12px;">Re-runs SessionStart/browser-error-daemon.js — spawns Chrome with --cdp-endpoint and registers the daemon pidfile.</span></div>' : ''}
1298
1307
  </div>
1299
1308
  <h2 style="margin-top:18px">Stale lockfiles</h2>
1300
1309
  \${lockfiles}\`;
@@ -1404,8 +1413,32 @@ document.addEventListener('click', (e) => {
1404
1413
  if (!confirm('Delete ' + total + ' captured log event(s) and truncate .claude/tmp/hooks.log?\\n\\nThis cannot be undone. (Future events will be captured normally.)')) return;
1405
1414
  return clearLogsAction();
1406
1415
  }
1416
+ if (t.id === 'restart-observer') {
1417
+ return restartObserverAction(t);
1418
+ }
1407
1419
  });
1408
1420
 
1421
+ async function restartObserverAction(btn) {
1422
+ const originalLabel = btn.textContent;
1423
+ btn.disabled = true;
1424
+ btn.textContent = 'Restarting…';
1425
+ try {
1426
+ const r = await fetch('/api/observer/restart', { method: 'POST' });
1427
+ const result = await r.json();
1428
+ if (r.ok && result.ok) {
1429
+ showToast('Observer restarted — pid ' + (result.observer && result.observer.pid) + '. Refreshing…', 'ok');
1430
+ } else {
1431
+ showToast('Restart failed: ' + (result.error || result.script_stderr || 'unknown'), 'err');
1432
+ }
1433
+ } catch (e) {
1434
+ showToast('Network error: ' + e.message, 'err');
1435
+ } finally {
1436
+ btn.disabled = false;
1437
+ btn.textContent = originalLabel;
1438
+ await refresh();
1439
+ }
1440
+ }
1441
+
1409
1442
  async function clearLogsAction() {
1410
1443
  try {
1411
1444
  const r = await fetch('/api/logs', { method: 'DELETE' });
@@ -1656,6 +1689,32 @@ function handler(cwd) {
1656
1689
  return send(status, JSON.stringify(result), 'application/json');
1657
1690
  }
1658
1691
 
1692
+ if (req.method === 'POST' && route === '/api/observer/restart') {
1693
+ // Re-run the SessionStart/browser-error-daemon.js hook script
1694
+ // synchronously. The script calls ensureObserver() which spawns
1695
+ // Chrome with --cdp-endpoint as a detached process. Once the
1696
+ // script exits, the daemon is up (or the script logged the failure).
1697
+ const daemonScript = path.join(cwd, '.claude', 'hooks', 'SessionStart', 'browser-error-daemon.js');
1698
+ if (!fs.existsSync(daemonScript)) {
1699
+ return send(404, JSON.stringify({ ok: false, error: 'daemon script not found at .claude/hooks/SessionStart/browser-error-daemon.js' }), 'application/json');
1700
+ }
1701
+ const { spawnSync } = require('child_process');
1702
+ const r = spawnSync(process.execPath, [daemonScript], {
1703
+ cwd,
1704
+ env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
1705
+ encoding: 'utf8',
1706
+ timeout: 15000,
1707
+ });
1708
+ const observerState = readObserverState(cwd, deriveCdpPort(cwd));
1709
+ const ok = observerState.running;
1710
+ return send(ok ? 200 : 500, JSON.stringify({
1711
+ ok,
1712
+ observer: observerState,
1713
+ script_exit: r.status,
1714
+ script_stderr: (r.stderr || '').slice(0, 500),
1715
+ }), 'application/json');
1716
+ }
1717
+
1659
1718
  if (req.method === 'POST' && route === '/api/reminders/toggle') {
1660
1719
  const raw = await readRequestBody(req);
1661
1720
  let payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.55",
3
+ "version": "2.13.57",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"