/* - Date: 2026-06-29 Spec: plans/2026-06-29-claude-code-raw-replay-harness-plan.md Relationship: Promotion-time normalizer for real raw agent replay captures. Capture files stay exact in /tmp; this script produces deterministic fixtures by normalizing machine-local paths and volatile run/session identifiers. */ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; import { basename, dirname, extname, join, resolve } from 'node:path'; export type AgentReplayNormalizeOptions = { readonly normalizeProviderIds: boolean; }; type NormalizerState = { readonly options: AgentReplayNormalizeOptions; readonly mappings: Map; nextRunId: number; nextSessionId: number; nextMessageId: number; }; type CliOptions = | { readonly type: 'single'; readonly input: string; readonly output: string; readonly normalizeProviderIds: boolean; } | { readonly type: 'directory'; readonly inputDir: string; readonly outputDir: string; readonly normalizeProviderIds: boolean; }; const defaultOptions: AgentReplayNormalizeOptions = { normalizeProviderIds: true, }; const homePathPattern = /\/home\/([A-Za-z0-9._-]+)/gu; const tmpRunPathPattern = /\/tmp\/linzumi-(codex|claude)-fixture-[A-Za-z0-9]+/gu; const tmpReplayPathPattern = /\/tmp\/linzumi-agent-replay-[A-Za-z0-9._-]+/gu; const codexSessionFilePattern = /rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-[0-9a-f-]{36}\.jsonl/giu; const uuidPattern = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/giu; const codexMessageIdPattern = /\bmsg_[A-Za-z0-9_-]{16,}\b/gu; const claudeSignaturePattern = /("signature"\s*:\s*")[^"]+(")/gu; export function normalizeAgentReplayText( text: string, options: AgentReplayNormalizeOptions = defaultOptions ): string { const state: NormalizerState = { options, mappings: new Map(), nextRunId: 1, nextSessionId: 1, nextMessageId: 1, }; return text .split(/\r?\n/u) .map((line) => normalizeNdjsonLine(line, state)) .join('\n'); } function normalizeNdjsonLine(line: string, state: NormalizerState): string { if (line.trim() === '') { return line; } try { const parsed = JSON.parse(line) as unknown; return JSON.stringify(normalizeJsonValue(parsed, state)); } catch (_error) { return normalizeString(line, state); } } function normalizeJsonValue(value: unknown, state: NormalizerState): unknown { if (Array.isArray(value)) { return value.map((item) => normalizeJsonValue(item, state)); } if (value !== null && typeof value === 'object') { const normalizedEntries = Object.entries(value).map(([key, child]) => { if (key === 'rawText' && typeof child === 'string') { return [key, normalizeRawText(child, state)] as const; } if (key === 'signature' && typeof child === 'string') { return [key, '[fixture-signature]'] as const; } return [key, normalizeJsonValue(child, state)] as const; }); return Object.fromEntries(normalizedEntries); } return typeof value === 'string' ? normalizeString(value, state) : value; } function normalizeRawText(value: string, state: NormalizerState): string { try { const parsed = JSON.parse(value) as unknown; return JSON.stringify(normalizeJsonValue(parsed, state)); } catch (_error) { return normalizeString(value, state); } } function normalizeString(value: string, state: NormalizerState): string { let normalized = value .replace(tmpRunPathPattern, (_match, provider: string) => mappedValue( state, _match, () => `/tmp/linzumi-agent-fixture/${provider}-run-${state.nextRunId++}` ) ) .replace(tmpReplayPathPattern, '/tmp/linzumi-agent-replay') .replace(homePathPattern, '/home/fixture-user') .replace(codexSessionFilePattern, (match) => mappedValue( state, match, () => `rollout-2026-06-29T00-00-00-fixture-session-${state.nextSessionId++}.jsonl` ) ) .replace(claudeSignaturePattern, '$1[fixture-signature]$2'); switch (state.options.normalizeProviderIds) { case true: normalized = normalized .replace(uuidPattern, (match) => mappedValue( state, match, () => `00000000-0000-4000-8000-${String(state.nextSessionId++).padStart(12, '0')}` ) ) .replace(codexMessageIdPattern, (match) => mappedValue( state, match, () => `msg_fixture_${String(state.nextMessageId++).padStart(4, '0')}` ) ); break; case false: break; } return normalized; } function mappedValue( state: NormalizerState, input: string, create: () => string ): string { const existing = state.mappings.get(input); if (existing !== undefined) { return existing; } const next = create(); state.mappings.set(input, next); return next; } async function main(): Promise { const args = process.argv.slice(2); if (args.includes('--help')) { printHelp(); return; } const options = parseCliOptions(args); switch (options.type) { case 'single': await normalizeFile(options.input, options.output, { normalizeProviderIds: options.normalizeProviderIds, }); process.stdout.write(`wrote ${options.output}\n`); break; case 'directory': { await mkdir(options.outputDir, { recursive: true }); const entries = await readdir(options.inputDir, { withFileTypes: true }); const ndjsonFiles = entries.filter( (entry) => entry.isFile() && extname(entry.name) === '.ndjson' ); for (const entry of ndjsonFiles) { const input = join(options.inputDir, entry.name); const output = join(options.outputDir, entry.name); await normalizeFile(input, output, { normalizeProviderIds: options.normalizeProviderIds, }); process.stdout.write(`wrote ${output}\n`); } break; } } } async function normalizeFile( input: string, output: string, options: AgentReplayNormalizeOptions ): Promise { const source = await readFile(input, 'utf8'); const normalized = normalizeAgentReplayText(source, options); await mkdir(dirname(output), { recursive: true }); await writeFile(output, normalized, 'utf8'); } function parseCliOptions(args: readonly string[]): CliOptions { const normalizeProviderIds = !args.includes('--keep-provider-ids'); const input = argValue(args, '--input'); const output = argValue(args, '--output'); const inputDir = argValue(args, '--input-dir'); const outputDir = argValue(args, '--output-dir'); if (input !== undefined && output !== undefined) { return { type: 'single', input: resolve(input), output: resolve(output), normalizeProviderIds, }; } if (inputDir !== undefined && outputDir !== undefined) { return { type: 'directory', inputDir: resolve(inputDir), outputDir: resolve(outputDir), normalizeProviderIds, }; } throw new Error( 'provide either --input/--output or --input-dir/--output-dir' ); } function argValue(args: readonly string[], name: string): string | undefined { const index = args.indexOf(name); if (index === -1) { return undefined; } return args[index + 1]; } function printHelp(): void { process.stdout.write( `Usage: pnpm run normalize:agent-fixture -- [options]\n\nOptions:\n --input --output Normalize one NDJSON fixture\n --input-dir --output-dir Normalize every .ndjson file in a directory\n --keep-provider-ids Preserve provider session/message ids\n\nExamples:\n pnpm run normalize:agent-fixture -- --input-dir /tmp/linzumi-agent-replay-probe --output-dir test/fixtures/agent-raw-replay/promoted\n` ); } if (basename(process.argv[1] ?? '') === 'normalize-agent-replay-fixture.ts') { main().catch((error) => { process.stderr.write( `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n` ); process.exitCode = 1; }); }