t3-code-android-nightly/apps/server/scripts/record-claude-agent-sdk-replay-fixture.ts
Julius Marminge b66dd402e8
refactor(provider-core): follow the Effect service conventions throughout (#17427)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:15:21 -07:00

606 lines
23 KiB
TypeScript

import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Console from "effect/Console";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import {
recordClaudeAgentSdkReplayTranscript,
CLAUDE_AGENT_SDK_REPLAY_PROTOCOL,
} from "../src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts";
import { claudeRuntimeQueryPolicyForRuntimePolicy } from "../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import type { RuntimePolicyV2Override } from "../src/orchestration-v2/RuntimePolicy.ts";
import { makeCheckpointWorkspace } from "@t3tools/provider-testing/replayWorkspace";
import { CLAUDE_MODEL_SELECTION } from "../src/orchestration-v2/testkit/fixtures/shared.ts";
import {
MESSAGE_STEERING_INITIAL_PROMPT,
MULTI_TURN_FIRST_PROMPT,
MESSAGE_STEERING_STEER_PROMPT,
READ_ONLY_NEVER_POLICY,
READ_ONLY_ON_REQUEST_POLICY,
RESTRICTED_GRANULAR_POLICY,
MULTI_TURN_SECOND_PROMPT,
SIMPLE_PROMPT,
SUBAGENT_PROMPT,
THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT,
THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT,
THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT,
THREAD_FORK_NATIVE_CONTINUE_FIRST_PROMPT,
THREAD_FORK_NATIVE_CONTINUE_SECOND_PROMPT,
THREAD_FORK_NATIVE_CONTINUE_SOURCE_PROMPT,
THREAD_FORK_NATIVE_SIBLINGS_FIRST_PROMPT,
THREAD_FORK_NATIVE_SIBLINGS_SECOND_PROMPT,
THREAD_FORK_NATIVE_SIBLINGS_SOURCE_PROMPT,
THREAD_FORK_NATIVE_SOURCE_PROMPT,
THREAD_FORK_NATIVE_TARGET_PROMPT,
THREAD_MERGE_BACK_FORK_PROMPT,
THREAD_MERGE_BACK_HANDOFF_PROMPT,
THREAD_MERGE_BACK_RECALL_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT,
THREAD_MERGE_BACK_SOURCE_PROMPT,
THREAD_ROLLBACK_AFTER_PROMPT,
THREAD_ROLLBACK_FIRST_PROMPT,
THREAD_ROLLBACK_SECOND_PROMPT,
TOOL_CALL_READ_ONLY_PROMPT,
TOOL_CALL_READ_ONLY_WORKSPACE_ROOT,
TOOL_CALL_WRITE_PROMPT,
TURN_INTERRUPT_MID_TOOL_PROMPT,
TURN_INTERRUPT_PROMPT,
TURN_INTERRUPT_RECOVERY_PROMPT,
WORKSPACE_NEVER_POLICY,
WEB_SEARCH_PROMPT,
} from "../src/orchestration-v2/testkit/fixtures/shared.ts";
import { CLAUDE_BACKGROUND_SUBAGENT_AFTER_ROOT_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_subagent_after_root/input.ts";
import {
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_FINAL_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_LAUNCH_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_RESUME_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT,
} from "../src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/input.ts";
import { CLAUDE_BACKGROUND_MONITOR_WAKE_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_monitor_wake/input.ts";
import { CLAUDE_NESTED_BACKGROUND_SUBAGENT_WAKE_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_nested_background_subagent_wake/input.ts";
import { CLAUDE_NESTED_SUBAGENT_MODEL_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_nested_subagent_model/input.ts";
import { CLAUDE_MCP_TOOL_PRESENTATION_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts";
import { CLAUDE_BACKGROUND_TASK_INTERRUPT_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_task_interrupt/input.ts";
import { CLAUDE_BACKGROUND_WAKE_BEFORE_QUEUED_PROMPT_LAUNCH_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_wake_before_queued_prompt/input.ts";
import {
CLAUDE_BACKGROUND_TASK_WAKE_FOLLOW_UP_PROMPT,
CLAUDE_BACKGROUND_TASK_WAKE_PROMPT,
} from "../src/orchestration-v2/testkit/fixtures/claude_background_task_wake/input.ts";
import {
DENIED_WRITE_POLICY,
TOOL_CALL_DENIED_WRITE_PROMPT,
TOOL_CALL_DENIED_WRITE_TARGET,
} from "../src/orchestration-v2/testkit/fixtures/tool_call_denied_write/input.ts";
import {
validateClaudeReplayRecordingSelection,
type ClaudeRecordingQueryMode,
} from "./claudeReplayRecordingConfig.ts";
const CLAUDE_RECORDINGS = {
simple: {
prompts: [SIMPLE_PROMPT],
defaultTranscriptFile: "fixtures/simple/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
multi_turn: {
prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT],
defaultTranscriptFile: "fixtures/multi_turn/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
multi_turn_restart: {
prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT],
defaultTranscriptFile: "fixtures/multi_turn_restart/claude_transcript.ndjson",
queryMode: "restart",
enableTools: true,
},
queued_turn: {
prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT],
defaultTranscriptFile: "fixtures/queued_turn/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
message_steering: {
prompts: [MESSAGE_STEERING_INITIAL_PROMPT, MESSAGE_STEERING_STEER_PROMPT],
defaultTranscriptFile: "fixtures/message_steering/claude_transcript.ndjson",
queryMode: "active_steering",
enableTools: true,
},
turn_interrupt_mid_tool: {
prompts: [TURN_INTERRUPT_MID_TOOL_PROMPT],
defaultTranscriptFile: "fixtures/turn_interrupt_mid_tool/claude_transcript.ndjson",
queryMode: "interrupt",
enableTools: true,
interruptAfter: "tool_use",
},
turn_interrupt: {
prompts: [TURN_INTERRUPT_PROMPT],
defaultTranscriptFile: "fixtures/turn_interrupt/claude_transcript.ndjson",
queryMode: "interrupt",
enableTools: true,
},
turn_interrupt_restart: {
prompts: [TURN_INTERRUPT_MID_TOOL_PROMPT, TURN_INTERRUPT_RECOVERY_PROMPT],
defaultTranscriptFile: "fixtures/turn_interrupt_restart/claude_transcript.ndjson",
queryMode: "interrupt_restart",
enableTools: true,
interruptAfter: "tool_use",
},
tool_call_read_only: {
prompts: [TOOL_CALL_READ_ONLY_PROMPT],
defaultTranscriptFile: "fixtures/tool_call_read_only/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
runtimePolicyOverride: READ_ONLY_NEVER_POLICY,
},
tool_call_read_only_on_request: {
prompts: [TOOL_CALL_WRITE_PROMPT],
defaultTranscriptFile: "fixtures/tool_call_read_only_on_request/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY,
},
tool_call_workspace_never: {
prompts: [TOOL_CALL_WRITE_PROMPT],
defaultTranscriptFile: "fixtures/tool_call_workspace_never/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
runtimePolicyOverride: WORKSPACE_NEVER_POLICY,
},
tool_call_denied_write: {
prompts: [TOOL_CALL_DENIED_WRITE_PROMPT],
defaultTranscriptFile: "fixtures/tool_call_denied_write/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
runtimePolicyOverride: DENIED_WRITE_POLICY,
permissionDecision: "decline",
expectedAbsentWorkspacePaths: [TOOL_CALL_DENIED_WRITE_TARGET],
},
tool_call_restricted_granular: {
prompts: [TOOL_CALL_WRITE_PROMPT],
defaultTranscriptFile: "fixtures/tool_call_restricted_granular/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
runtimePolicyOverride: RESTRICTED_GRANULAR_POLICY,
},
web_search: {
prompts: [WEB_SEARCH_PROMPT],
defaultTranscriptFile: "fixtures/web_search/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
claude_background_subagent_after_root: {
prompts: [CLAUDE_BACKGROUND_SUBAGENT_AFTER_ROOT_PROMPT],
defaultTranscriptFile:
"fixtures/claude_background_subagent_after_root/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
backgroundWakeCounts: [1],
},
claude_background_task_wake: {
prompts: [CLAUDE_BACKGROUND_TASK_WAKE_PROMPT, CLAUDE_BACKGROUND_TASK_WAKE_FOLLOW_UP_PROMPT],
defaultTranscriptFile: "fixtures/claude_background_task_wake/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
backgroundWakeCounts: [1, 0],
},
claude_background_monitor_wake: {
prompts: [CLAUDE_BACKGROUND_MONITOR_WAKE_PROMPT],
defaultTranscriptFile: "fixtures/claude_background_monitor_wake/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
backgroundWakeCounts: [1],
},
claude_background_subagent_lifecycle: {
prompts: [
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_LAUNCH_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_RESUME_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_FINAL_PROMPT,
],
defaultTranscriptFile: "fixtures/claude_background_subagent_lifecycle/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
backgroundWakeCounts: [1, 0, 1, 0],
},
// Each prompt is offered as soon as its turn and the wakes counted here
// settle, so a wake queued during a turn (Agent B's "stopped" notice) is
// still pending in the CLI when the next prompt arrives, and runs first.
claude_background_wake_before_queued_prompt: {
prompts: [
CLAUDE_BACKGROUND_WAKE_BEFORE_QUEUED_PROMPT_LAUNCH_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_RESUME_PROMPT,
CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_FINAL_PROMPT,
],
defaultTranscriptFile:
"fixtures/claude_background_wake_before_queued_prompt/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
backgroundWakeCounts: [1, 0, 1, 0],
offerNextPromptImmediately: true,
},
claude_background_task_interrupt: {
prompts: [CLAUDE_BACKGROUND_TASK_INTERRUPT_PROMPT],
defaultTranscriptFile: "fixtures/claude_background_task_interrupt/claude_transcript.ndjson",
queryMode: "interrupt",
enableTools: true,
interruptAfter: "tool_use",
interruptAfterToolUses: 2,
},
claude_nested_background_subagent_wake: {
prompts: [CLAUDE_NESTED_BACKGROUND_SUBAGENT_WAKE_PROMPT],
defaultTranscriptFile:
"fixtures/claude_nested_background_subagent_wake/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
backgroundWakeCounts: [1],
},
claude_nested_subagent_model: {
prompts: [CLAUDE_NESTED_SUBAGENT_MODEL_PROMPT],
defaultTranscriptFile: "fixtures/claude_nested_subagent_model/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
// Needs the claude.ai Firecrawl connector on the recording account. Claude
// Code describes MCP tool uses in an undeclared `tool_use_meta` field.
claude_mcp_tool_presentation: {
prompts: [CLAUDE_MCP_TOOL_PRESENTATION_PROMPT],
defaultTranscriptFile: "fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
subagent: {
prompts: [SUBAGENT_PROMPT],
defaultTranscriptFile: "fixtures/subagent/claude_transcript.ndjson",
queryMode: "streaming",
enableTools: true,
},
thread_rollback: {
prompts: [
THREAD_ROLLBACK_FIRST_PROMPT,
THREAD_ROLLBACK_SECOND_PROMPT,
THREAD_ROLLBACK_AFTER_PROMPT,
],
defaultTranscriptFile: "fixtures/thread_rollback/claude_transcript.ndjson",
queryMode: "resume_at_cursor",
enableTools: true,
},
thread_fork_native: {
prompts: [THREAD_FORK_NATIVE_SOURCE_PROMPT, THREAD_FORK_NATIVE_TARGET_PROMPT],
defaultTranscriptFile: "fixtures/thread_fork_native/claude_transcript.ndjson",
queryMode: "fork_session",
enableTools: true,
},
thread_fork_native_prior_turn: {
prompts: [
THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT,
THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT,
THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT,
],
defaultTranscriptFile: "fixtures/thread_fork_native_prior_turn/claude_transcript.ndjson",
queryMode: "fork_session_prior_turn",
enableTools: true,
},
thread_fork_native_continue: {
prompts: [
THREAD_FORK_NATIVE_CONTINUE_SOURCE_PROMPT,
THREAD_FORK_NATIVE_CONTINUE_FIRST_PROMPT,
THREAD_FORK_NATIVE_CONTINUE_SECOND_PROMPT,
],
defaultTranscriptFile: "fixtures/thread_fork_native_continue/claude_transcript.ndjson",
queryMode: "fork_session_continue",
enableTools: true,
},
thread_fork_native_siblings: {
prompts: [
THREAD_FORK_NATIVE_SIBLINGS_SOURCE_PROMPT,
THREAD_FORK_NATIVE_SIBLINGS_FIRST_PROMPT,
THREAD_FORK_NATIVE_SIBLINGS_SECOND_PROMPT,
],
defaultTranscriptFile: "fixtures/thread_fork_native_siblings/claude_transcript.ndjson",
queryMode: "fork_session_siblings",
enableTools: true,
},
thread_merge_back_continue: {
prompts: [
THREAD_MERGE_BACK_SOURCE_PROMPT,
THREAD_MERGE_BACK_FORK_PROMPT,
THREAD_MERGE_BACK_HANDOFF_PROMPT,
THREAD_MERGE_BACK_RECALL_PROMPT,
],
defaultTranscriptFile: "fixtures/thread_merge_back_continue/claude_transcript.ndjson",
queryMode: "fork_session_merge_back",
enableTools: true,
},
thread_merge_back_siblings: {
prompts: [
THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT,
THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT,
],
defaultTranscriptFile: "fixtures/thread_merge_back_siblings/claude_transcript.ndjson",
queryMode: "fork_session_merge_back_siblings",
enableTools: true,
},
} as const;
function readArgValue(name: string): string | undefined {
const args = process.argv.slice(2);
const index = args.indexOf(name);
return index >= 0 ? args[index + 1] : undefined;
}
function selectedQueryMode(defaultMode: ClaudeRecordingQueryMode): ClaudeRecordingQueryMode {
const raw = readArgValue("--query-mode") ?? process.env.T3_CLAUDE_REPLAY_QUERY_MODE;
if (raw === undefined) {
return defaultMode;
}
if (
raw === "streaming" ||
raw === "restart" ||
raw === "resume_at_cursor" ||
raw === "fork_session" ||
raw === "fork_session_prior_turn" ||
raw === "fork_session_continue" ||
raw === "fork_session_siblings" ||
raw === "fork_session_merge_back" ||
raw === "fork_session_merge_back_siblings" ||
raw === "active_steering" ||
raw === "interrupt" ||
raw === "interrupt_restart"
) {
return raw;
}
throw new Error(
`Unsupported Claude replay query mode '${raw}'. Use 'streaming', 'restart', 'resume_at_cursor', 'fork_session', 'fork_session_prior_turn', 'fork_session_continue', 'fork_session_siblings', 'fork_session_merge_back', 'fork_session_merge_back_siblings', 'active_steering', 'interrupt', or 'interrupt_restart'.`,
);
}
const scenario = readArgValue("--scenario") ?? process.env.T3_CLAUDE_REPLAY_SCENARIO ?? "simple";
const recording = CLAUDE_RECORDINGS[scenario as keyof typeof CLAUDE_RECORDINGS];
const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
if (recording === undefined) {
throw new Error(`Claude replay fixture '${scenario}' is not configured.`);
}
const positionalOutputPath = process.argv[2]?.startsWith("--") ? undefined : process.argv[2];
const path = await Effect.runPromise(
Effect.service(Path.Path).pipe(Effect.provide(NodeServices.layer)),
);
const outputPath =
readArgValue("--out") ??
positionalOutputPath ??
(await Effect.runPromise(
path.fromFileUrl(
new URL(
`../src/orchestration-v2/testkit/${recording.defaultTranscriptFile}`,
import.meta.url,
),
),
));
function encodeTranscriptNdjson(
transcript: Awaited<ReturnType<typeof recordClaudeAgentSdkReplayTranscript>>,
): string {
const { entries, ...metadata } = transcript;
return [
JSON.stringify({ type: "transcript_start", ...metadata }),
...entries.map((entry) => JSON.stringify(entry)),
"",
].join("\n");
}
function joinPath(directory: string, fileName: string): string {
return `${directory.replace(/\/+$/u, "")}/${fileName.replace(/^\/+/u, "")}`;
}
function runFileSystem<A, E>(effect: Effect.Effect<A, E, FileSystem.FileSystem>): Promise<A> {
return Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));
}
function selectedPrompts(): ReadonlyArray<string> {
if (process.env.T3_CLAUDE_REPLAY_PROMPTS !== undefined) {
return process.env.T3_CLAUDE_REPLAY_PROMPTS.split("\n---\n").filter(
(prompt) => prompt.length > 0,
);
}
if (process.env.T3_CLAUDE_REPLAY_PROMPT !== undefined) {
return [process.env.T3_CLAUDE_REPLAY_PROMPT];
}
return recording.prompts;
}
const prompts = selectedPrompts();
const queryMode = selectedQueryMode(recording.queryMode);
validateClaudeReplayRecordingSelection({
scenario,
configuredQueryMode: recording.queryMode,
selectedQueryMode: queryMode,
configuredPromptCount: recording.prompts.length,
selectedPromptCount: prompts.length,
});
function runtimePolicyForRecording(input: {
readonly cwd: string;
readonly override?: RuntimePolicyV2Override;
}): ProviderAdapter.ProviderAdapterV2RuntimePolicy {
return ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: input.override?.cwd ?? input.cwd,
...(input.override?.approvalPolicy === undefined
? {}
: { approvalPolicy: input.override.approvalPolicy }),
...(input.override?.sandboxPolicy === undefined
? {}
: { sandboxPolicy: input.override.sandboxPolicy }),
...(input.override?.reasoningEffort === undefined
? {}
: { reasoningEffort: input.override.reasoningEffort }),
});
}
async function makeToolCallReadOnlyRecordingWorkspace(): Promise<string> {
await runFileSystem(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.remove(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { recursive: true, force: true });
yield* fs.makeDirectory(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { recursive: true });
}),
);
return TOOL_CALL_READ_ONLY_WORKSPACE_ROOT;
}
const cwd =
process.env.T3_CLAUDE_REPLAY_CWD ??
(scenario === "tool_call_read_only"
? await makeToolCallReadOnlyRecordingWorkspace()
: await makeCheckpointWorkspace(`claude-agent-sdk-record-${scenario}`));
const shouldRemoveCwd = process.env.T3_CLAUDE_REPLAY_CWD === undefined;
const expectedAbsentWorkspacePaths =
"expectedAbsentWorkspacePaths" in recording ? recording.expectedAbsentWorkspacePaths : [];
// Scenarios that deny a tool call prove the denial by checking the real
// recording workspace, not just the transcript: the target must be absent
// before and after the turn, and the verified paths are preserved in the
// transcript metadata before the temporary workspace is removed.
async function assertWorkspacePathsAbsent(phase: "before" | "after"): Promise<void> {
const presentPaths = await runFileSystem(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const present: Array<string> = [];
for (const relativePath of expectedAbsentWorkspacePaths) {
if (yield* fs.exists(joinPath(cwd, relativePath))) {
present.push(relativePath);
}
}
return present;
}),
);
if (presentPaths.length > 0) {
throw new Error(
`Claude replay fixture '${scenario}' expected ${presentPaths
.map((relativePath) => `'${relativePath}'`)
.join(
", ",
)} to be absent ${phase} the recorded turn, but found in the recording workspace.`,
);
}
}
if (shouldRemoveCwd && (scenario === "tool_call_read_only" || scenario === "subagent")) {
await runFileSystem(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.writeFileString(
joinPath(cwd, "package.json"),
encodeUnknownJsonString({
name: "claude-read-only-fixture",
private: true,
scripts: { typecheck: "tsc --noEmit" },
}),
);
yield* fs.writeFileString(
joinPath(cwd, "tsconfig.json"),
encodeUnknownJsonString({
compilerOptions: {
module: "ESNext",
strict: true,
target: "ES2022",
},
}),
);
}),
);
}
try {
const runtimePolicy = runtimePolicyForRecording({
cwd,
...("runtimePolicyOverride" in recording ? { override: recording.runtimePolicyOverride } : {}),
});
const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy(runtimePolicy);
await assertWorkspacePathsAbsent("before");
const transcript = await recordClaudeAgentSdkReplayTranscript({
scenario,
prompts,
modelSelection: {
...CLAUDE_MODEL_SELECTION,
model: process.env.T3_CLAUDE_REPLAY_MODEL ?? CLAUDE_MODEL_SELECTION.model,
},
cwd,
...(process.env.T3_CLAUDE_REPLAY_SESSION_ID === undefined
? {}
: { sessionId: process.env.T3_CLAUDE_REPLAY_SESSION_ID }),
queryMode,
...("enableTools" in recording && recording.enableTools === true ? { enableTools: true } : {}),
...(queryPolicy.tools === undefined ? {} : { tools: queryPolicy.tools }),
permissionMode: queryPolicy.permissionMode,
...(queryPolicy.allowedTools === undefined ? {} : { allowedTools: queryPolicy.allowedTools }),
...(queryPolicy.allowDangerouslySkipPermissions === undefined
? {}
: { allowDangerouslySkipPermissions: queryPolicy.allowDangerouslySkipPermissions }),
...(queryPolicy.installPermissionCallback ? { enablePermissionCallback: true } : {}),
...("permissionDecision" in recording
? { permissionDecision: recording.permissionDecision }
: {}),
...("interruptAfter" in recording ? { interruptAfter: recording.interruptAfter } : {}),
...("interruptAfterToolUses" in recording
? { interruptAfterToolUses: recording.interruptAfterToolUses }
: {}),
...("backgroundWakeCounts" in recording
? { backgroundWakeCounts: recording.backgroundWakeCounts }
: {}),
...("offerNextPromptImmediately" in recording
? { offerNextPromptImmediately: recording.offerNextPromptImmediately }
: {}),
});
await assertWorkspacePathsAbsent("after");
const transcriptWithEvidence =
expectedAbsentWorkspacePaths.length === 0
? transcript
: {
...transcript,
metadata: {
...transcript.metadata,
verifiedAbsentWorkspacePaths: [...expectedAbsentWorkspacePaths],
},
};
await runFileSystem(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true });
yield* fs.writeFileString(outputPath, encodeTranscriptNdjson(transcriptWithEvidence));
}),
);
await Effect.runPromise(
Console.log(
`Wrote ${transcript.entries.length} ${CLAUDE_AGENT_SDK_REPLAY_PROTOCOL} replay entries to ${outputPath}`,
),
);
} finally {
if (shouldRemoveCwd) {
await runFileSystem(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.remove(cwd, { recursive: true, force: true });
}),
);
}
}