/* - Date: 2026-06-29 Spec: plans/2026-06-29-agent-backend-replay-orchestrator-spec.md Relationship: One-command local browser proof wrapper for the real backend replay path. It runs replay-agent-backend first, then points Playwright at the real frontend thread route and backend-produced summary artifact. */ import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const scriptDir = dirname(fileURLToPath(import.meta.url)); const packageRoot = resolve(scriptDir, '..'); const repoRoot = resolve(packageRoot, '..', '..'); const webRoot = join(repoRoot, 'kandan', 'server_v2', 'web'); type BrowserReplayOptions = { readonly backendArgs: readonly string[]; readonly backendUrl: string; readonly outDir: string; readonly summaryPath: string; readonly browserAuthPath: string; readonly explicitBrowserAuthPath: boolean; }; async function main(): Promise { const args = stripLeadingSeparator(process.argv.slice(2)); if (args.includes('--help')) { printHelp(); return; } const options = parseArgs(args); await mkdir(options.outDir, { recursive: true }); await runBackendReplay(options); await assertReplayArtifacts(options); await runBrowserProof(options); } function stripLeadingSeparator(args: readonly string[]): readonly string[] { return args[0] === '--' ? args.slice(1) : args; } function parseArgs(args: readonly string[]): BrowserReplayOptions { const backendUrl = argValue(args, '--backend-url'); if (backendUrl === undefined) { throw new Error('provide --backend-url '); } if (args.includes('--dry-run')) { throw new Error( 'browser proof requires a real backend replay; remove --dry-run' ); } if (args.includes('--probe-backend')) { throw new Error( 'browser proof requires a real backend replay; remove --probe-backend' ); } const outDir = resolve( argValue(args, '--out-dir') ?? 'agent-backend-browser-replay-output' ); const explicitBrowserAuth = argValue(args, '--browser-auth-file'); const browserAuthPath = resolve( explicitBrowserAuth ?? join(outDir, 'browser-auth.json') ); const backendArgs = explicitBrowserAuth === undefined ? [...args, '--browser-auth-file', browserAuthPath] : [...args]; return { backendArgs, backendUrl, outDir, summaryPath: join(outDir, 'summary.json'), browserAuthPath, explicitBrowserAuthPath: explicitBrowserAuth !== undefined, }; } async function runBackendReplay(options: BrowserReplayOptions): Promise { await runCommand( 'pnpm', [ '--filter', '@linzumi/cli', 'run', 'replay:agent-backend', '--', ...options.backendArgs, ], repoRoot, process.env ); } async function assertReplayArtifacts( options: BrowserReplayOptions ): Promise { if (!existsSync(options.summaryPath)) { throw new Error( `backend replay did not write summary artifact: ${options.summaryPath}` ); } if (!existsSync(options.browserAuthPath)) { const hint = options.explicitBrowserAuthPath ? 'check --browser-auth-file path' : 'the wrapper should have passed --browser-auth-file automatically'; throw new Error( `backend replay did not write browser auth artifact: ${options.browserAuthPath}; ${hint}` ); } } async function runBrowserProof(options: BrowserReplayOptions): Promise { await runCommand( 'bun', ['run', 'replay-harness:agent-backend-browser'], webRoot, { ...process.env, KANDAN_REPLAY_BASE_URL: options.backendUrl, KANDAN_AGENT_BACKEND_REPLAY_SUMMARY: options.summaryPath, KANDAN_AGENT_BACKEND_REPLAY_BROWSER_AUTH: options.browserAuthPath, } ); } function runCommand( command: string, args: readonly string[], cwd: string, env: NodeJS.ProcessEnv ): Promise { return new Promise((resolvePromise, reject) => { const child = spawn(command, args, { cwd, env, stdio: 'inherit', }); child.on('error', reject); child.on('exit', (code, signal) => { if (code === 0) { resolvePromise(); return; } reject( new Error( `${command} ${args.join(' ')} failed with ${ signal === null ? `exit code ${code ?? 'unknown'}` : `signal ${signal}` }` ) ); }); }); } function argValue(args: readonly string[], name: string): string | undefined { const index = args.indexOf(name); if (index === -1) { return undefined; } const value = args[index + 1]; if (value === undefined) { throw new Error(`${name} requires a value`); } return value; } function printHelp(): void { process.stdout.write( `Usage: pnpm --filter @linzumi/cli run replay:agent-backend-browser -- --fixture --backend-url [backend replay options]\n\nThis runs the real backend replay first, then runs the Kandan web replay harness\nPlaywright browser proof against the generated summary.json and browser auth\nartifact. It forwards backend replay options to replay:agent-backend.\n\nRequired:\n --fixture Agent replay fixture\n --backend-url Running local Phoenix backend URL\n\nCommon options forwarded to replay:agent-backend:\n --provider auto|codex|claude-code\n --setup-fixture\n --out-dir Default: agent-backend-browser-replay-output\n --speed immediate|realtime|x|ms\n --require-feature Forwarded per-feature manifest/backend oracle gate --oracle-feature Candidate proof oracle without readiness gating\n --browser-auth-file Optional; default: /browser-auth.json\n\nExample:\n pnpm --filter @linzumi/cli run replay:agent-backend-browser -- \\\n --fixture packages/linzumi-cli/test/fixtures/agent-raw-replay/real-probe/claude.ndjson \\\n --provider claude-code \\\n --backend-url http://127.0.0.1:4140 \\\n --setup-fixture \\\n --out-dir /tmp/agent-backend-replay-browser\n` ); } main().catch((error) => { process.stderr.write( `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n` ); process.exitCode = 1; });