// Agent bridge — NDJSON stdio interface for Claude Agent SDK integration // Accepts queries on stdin, responds on stdout. Allows an agent to control // and query the test daemon programmatically. import { createInterface } from 'node:readline'; import type { Dashboard } from './dashboard.ts'; import { clearCache, type RunOptions } from './runner.ts'; // ── Query/Response types ── type Query = | { type: 'status' } | { type: 'rerun'; pattern?: string } | { type: 'failures' } | { type: 'reset-cache' }; type Response = | { type: 'status'; running: boolean; passed: number; failed: number; skipped: number; pending: number; total: number; failures: Array<{ name: string; error?: string }> } | { type: 'rerun'; specsQueued: number } | { type: 'failures'; failures: Array<{ name: string; specFile: string; error?: string }> } | { type: 'reset-cache'; ok: true } | { type: 'error'; message: string }; // ── Bridge ── export class AgentBridge { private dashboard: Dashboard; private running = false; private triggerRerun: ((opts: RunOptions) => void) | null = null; private rl: ReturnType | null = null; constructor(dashboard: Dashboard) { this.dashboard = dashboard; } /** Register callback that triggers a new test run from the main loop */ onRerunRequest(cb: (opts: RunOptions) => void): void { this.triggerRerun = cb; } setRunning(running: boolean): void { this.running = running; } start(): void { this.rl = createInterface({ input: process.stdin, terminal: false, }); this.rl.on('line', (line) => { const trimmed = line.trim(); if (!trimmed) return; try { const query = JSON.parse(trimmed) as Query; const response = this.handleQuery(query); this.send(response); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); this.send({ type: 'error', message: `Invalid query: ${msg}` }); } }); this.rl.on('close', () => { this.stop(); }); } stop(): void { if (this.rl) { this.rl.close(); this.rl = null; } } private send(response: Response): void { // Write to stdout as NDJSON — use fd 3 or stderr if stdout is used by dashboard // Since dashboard writes to stdout, we use stderr for NDJSON responses // when the dashboard is active. The agent reads from our stderr. process.stderr.write(JSON.stringify(response) + '\n'); } private handleQuery(query: Query): Response { switch (query.type) { case 'status': return this.handleStatus(); case 'rerun': return this.handleRerun(query); case 'failures': return this.handleFailures(); case 'reset-cache': return this.handleResetCache(); default: return { type: 'error', message: `Unknown query type: ${(query as { type: string }).type}` }; } } private handleStatus(): Response { const tests = this.dashboard.getTests(); const passed = tests.filter((t) => t.status === 'passed').length; const failed = tests.filter((t) => t.status === 'failed').length; const skipped = tests.filter((t) => t.status === 'skipped').length; const pending = tests.filter((t) => t.status === 'pending' || t.status === 'running').length; const failures = tests .filter((t) => t.status === 'failed') .map((t) => ({ name: t.name, error: t.error })); return { type: 'status', running: this.running, passed, failed, skipped, pending, total: tests.length, failures, }; } private handleRerun(query: Extract): Response { if (this.running) return { type: 'rerun', specsQueued: 0 }; const opts: RunOptions = { full: true }; if (query.pattern) opts.pattern = query.pattern; this.triggerRerun?.(opts); return { type: 'rerun', specsQueued: 1 }; } private handleFailures(): Response { const tests = this.dashboard.getTests(); const failures = tests .filter((t) => t.status === 'failed') .map((t) => ({ name: t.name, specFile: t.specFile, error: t.error, })); return { type: 'failures', failures }; } private handleResetCache(): Response { clearCache(); return { type: 'reset-cache', ok: true }; } }