/** * Cassette Normalizer * * Strips dynamic content from prompts and CLI args before hashing into a cassette key. * Dynamic content (UUIDs, temp paths, timestamps, session numbers) varies between * test runs but doesn't affect how the agent responds — so we replace them with * stable placeholders to get a stable cache key. */ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; const NANOID_RE = /(? (with or without trailing slash) // The agent name (e.g. "available-sheep") changes every run but is not a UUID or nanoid. // Stop at the first slash so the project name after it is preserved. const AGENT_WORKDIR_RE = /agent-workdirs\/[^\s/\\]+/g; /** * Normalize a prompt for stable cassette key generation. * * Replacements applied in order (most-specific first to avoid partial matches): * 1. Absolute workspace root path → __WORKSPACE__ * 2. UUIDs → __UUID__ * 2.5. Nanoid IDs (21-char alphanumeric) → __ID__ * 3. ISO 8601 timestamps → __TIMESTAMP__ * 4. Unix epoch milliseconds → __EPOCH__ * 5. Session numbers → session__N__ * 6. Agent worktree path segment → agent-workdirs/__AGENT__/ */ export function normalizePrompt(prompt: string, workspaceRoot: string): string { let normalized = prompt; if (workspaceRoot) { normalized = normalized.replaceAll(workspaceRoot, '__WORKSPACE__'); } normalized = normalized.replace(UUID_RE, '__UUID__'); normalized = normalized.replace(NANOID_RE, '__ID__'); normalized = normalized.replace(ISO_TIMESTAMP_RE, '__TIMESTAMP__'); normalized = normalized.replace(UNIX_EPOCH_MS_RE, '__EPOCH__'); normalized = normalized.replace(SESSION_NUM_RE, 'session__N__'); normalized = normalized.replace(AGENT_WORKDIR_RE, 'agent-workdirs/__AGENT__'); return normalized; } /** * Strip the prompt value from CLI args to produce stable modelArgs for the cassette key. * * Handles all provider prompt flag styles: * - Native: `-p ` (Claude) * - Flag: `--prompt `, `-p ` (Gemini, Cursor, Auggie, Amp, Opencode) * - Also removes the bare prompt value if it appears as a positional arg. */ export function stripPromptFromArgs(args: string[], prompt: string): string[] { if (!prompt) return [...args]; const result: string[] = []; let i = 0; while (i < args.length) { const arg = args[i]; const PROMPT_FLAGS = ['-p', '--prompt', '--message']; if (PROMPT_FLAGS.includes(arg) && args[i + 1] === prompt) { i += 2; // skip flag + value } else if (arg === prompt) { i += 1; // skip bare positional prompt } else { result.push(arg); i++; } } return result; }