Skip to content
Source
Sign inSign up

record-claude-agent-sdk-replay-fixture.ts

561 lines21.0 KB

1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as Console from "effect/Console";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as Schema from "effect/Schema";
7​
8import {
9 recordClaudeAgentSdkReplayTranscript,
10 CLAUDE_AGENT_SDK_REPLAY_PROTOCOL,
11} from "../src/orchestration-v2/Adapters/ClaudeAdapterV2.testkit.ts";
12import { claudeRuntimeQueryPolicyForRuntimePolicy } from "../src/orchestration-v2/Adapters/ClaudeAdapterV2.ts";
13import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
14import type { RuntimePolicyV2Override } from "../src/orchestration-v2/RuntimePolicy.ts";
15import { makeCheckpointWorkspace } from "@t3tools/provider-testing/replayWorkspace";
16import { CLAUDE_MODEL_SELECTION } from "../src/orchestration-v2/testkit/fixtures/shared.ts";
17import {
18 MESSAGE_STEERING_INITIAL_PROMPT,
19 MULTI_TURN_FIRST_PROMPT,
20 MESSAGE_STEERING_STEER_PROMPT,
21 READ_ONLY_NEVER_POLICY,
22 READ_ONLY_ON_REQUEST_POLICY,
23 RESTRICTED_GRANULAR_POLICY,
24 MULTI_TURN_SECOND_PROMPT,
25 SIMPLE_PROMPT,
26 SUBAGENT_PROMPT,
27 THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT,
28 THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT,
29 THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT,
30 THREAD_FORK_NATIVE_SOURCE_PROMPT,
31 THREAD_FORK_NATIVE_TARGET_PROMPT,
32 THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT,
33 THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT,
34 THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT,
35 THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT,
36 THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT,
37 THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT,
38 THREAD_ROLLBACK_AFTER_PROMPT,
39 THREAD_ROLLBACK_FIRST_PROMPT,
40 THREAD_ROLLBACK_SECOND_PROMPT,
41 TOOL_CALL_READ_ONLY_PROMPT,
42 TOOL_CALL_READ_ONLY_WORKSPACE_ROOT,
43 TOOL_CALL_WRITE_PROMPT,
44 TURN_INTERRUPT_MID_TOOL_PROMPT,
45 COMMAND_OUTPUT_STREAMING_PROMPT,
46 TURN_INTERRUPT_PROMPT,
47 TURN_INTERRUPT_RECOVERY_PROMPT,
48 WORKSPACE_NEVER_POLICY,
49 WEB_SEARCH_PROMPT,
50} from "../src/orchestration-v2/testkit/fixtures/shared.ts";
51import { CLAUDE_BACKGROUND_SUBAGENT_AFTER_ROOT_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_subagent_after_root/input.ts";
52import {
53 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_FINAL_PROMPT,
54 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_LAUNCH_PROMPT,
55 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_RESUME_PROMPT,
56 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT,
57} from "../src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/input.ts";
58import { CLAUDE_BACKGROUND_MONITOR_WAKE_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_monitor_wake/input.ts";
59import { CLAUDE_NESTED_BACKGROUND_SUBAGENT_WAKE_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_nested_background_subagent_wake/input.ts";
60import { CLAUDE_NESTED_SUBAGENT_MODEL_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_nested_subagent_model/input.ts";
61import { CLAUDE_MCP_TOOL_PRESENTATION_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_mcp_tool_presentation/input.ts";
62import { CLAUDE_BACKGROUND_TASK_INTERRUPT_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_task_interrupt/input.ts";
63import { CLAUDE_BACKGROUND_WAKE_BEFORE_QUEUED_PROMPT_LAUNCH_PROMPT } from "../src/orchestration-v2/testkit/fixtures/claude_background_wake_before_queued_prompt/input.ts";
64import {
65 CLAUDE_BACKGROUND_TASK_WAKE_FOLLOW_UP_PROMPT,
66 CLAUDE_BACKGROUND_TASK_WAKE_PROMPT,
67} from "../src/orchestration-v2/testkit/fixtures/claude_background_task_wake/input.ts";
68import {
69 DENIED_WRITE_POLICY,
70 TOOL_CALL_DENIED_WRITE_PROMPT,
71 TOOL_CALL_DENIED_WRITE_TARGET,
72} from "../src/orchestration-v2/testkit/fixtures/tool_call_denied_write/input.ts";
73import {
74 validateClaudeReplayRecordingSelection,
75 type ClaudeRecordingQueryMode,
76} from "./claudeReplayRecordingConfig.ts";
77​
78const CLAUDE_RECORDINGS = {
79 simple: {
80 prompts: [SIMPLE_PROMPT],
81 defaultTranscriptFile: "fixtures/simple/claude_transcript.ndjson",
82 queryMode: "streaming",
83 enableTools: true,
84 },
85 multi_turn: {
86 prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT],
87 defaultTranscriptFile: "fixtures/multi_turn/claude_transcript.ndjson",
88 queryMode: "streaming",
89 enableTools: true,
90 },
91 multi_turn_restart: {
92 prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT],
93 defaultTranscriptFile: "fixtures/multi_turn_restart/claude_transcript.ndjson",
94 queryMode: "restart",
95 enableTools: true,
96 },
97 queued_turn: {
98 prompts: [MULTI_TURN_FIRST_PROMPT, MULTI_TURN_SECOND_PROMPT],
99 defaultTranscriptFile: "fixtures/queued_turn/claude_transcript.ndjson",
100 queryMode: "streaming",
101 enableTools: true,
102 },
103 message_steering: {
104 prompts: [MESSAGE_STEERING_INITIAL_PROMPT, MESSAGE_STEERING_STEER_PROMPT],
105 defaultTranscriptFile: "fixtures/message_steering/claude_transcript.ndjson",
106 queryMode: "active_steering",
107 enableTools: true,
108 },
109 command_output_streaming: {
110 prompts: [COMMAND_OUTPUT_STREAMING_PROMPT],
111 defaultTranscriptFile: "fixtures/command_output_streaming/claude_transcript.ndjson",
112 queryMode: "streaming",
113 enableTools: true,
114 },
115 turn_interrupt_mid_tool: {
116 prompts: [TURN_INTERRUPT_MID_TOOL_PROMPT],
117 defaultTranscriptFile: "fixtures/turn_interrupt_mid_tool/claude_transcript.ndjson",
118 queryMode: "interrupt",
119 enableTools: true,
120 interruptAfter: "tool_use",
121 },
122 turn_interrupt: {
123 prompts: [TURN_INTERRUPT_PROMPT],
124 defaultTranscriptFile: "fixtures/turn_interrupt/claude_transcript.ndjson",
125 queryMode: "interrupt",
126 enableTools: true,
127 },
128 turn_interrupt_restart: {
129 prompts: [TURN_INTERRUPT_MID_TOOL_PROMPT, TURN_INTERRUPT_RECOVERY_PROMPT],
130 defaultTranscriptFile: "fixtures/turn_interrupt_restart/claude_transcript.ndjson",
131 queryMode: "interrupt_restart",
132 enableTools: true,
133 interruptAfter: "tool_use",
134 },
135 tool_call_read_only: {
136 prompts: [TOOL_CALL_READ_ONLY_PROMPT],
137 defaultTranscriptFile: "fixtures/tool_call_read_only/claude_transcript.ndjson",
138 queryMode: "streaming",
139 enableTools: true,
140 runtimePolicyOverride: READ_ONLY_NEVER_POLICY,
141 },
142 tool_call_read_only_on_request: {
143 prompts: [TOOL_CALL_WRITE_PROMPT],
144 defaultTranscriptFile: "fixtures/tool_call_read_only_on_request/claude_transcript.ndjson",
145 queryMode: "streaming",
146 enableTools: true,
147 runtimePolicyOverride: READ_ONLY_ON_REQUEST_POLICY,
148 },
149 tool_call_workspace_never: {
150 prompts: [TOOL_CALL_WRITE_PROMPT],
151 defaultTranscriptFile: "fixtures/tool_call_workspace_never/claude_transcript.ndjson",
152 queryMode: "streaming",
153 enableTools: true,
154 runtimePolicyOverride: WORKSPACE_NEVER_POLICY,
155 },
156 tool_call_denied_write: {
157 prompts: [TOOL_CALL_DENIED_WRITE_PROMPT],
158 defaultTranscriptFile: "fixtures/tool_call_denied_write/claude_transcript.ndjson",
159 queryMode: "streaming",
160 enableTools: true,
161 runtimePolicyOverride: DENIED_WRITE_POLICY,
162 permissionDecision: "decline",
163 expectedAbsentWorkspacePaths: [TOOL_CALL_DENIED_WRITE_TARGET],
164 },
165 tool_call_restricted_granular: {
166 prompts: [TOOL_CALL_WRITE_PROMPT],
167 defaultTranscriptFile: "fixtures/tool_call_restricted_granular/claude_transcript.ndjson",
168 queryMode: "streaming",
169 enableTools: true,
170 runtimePolicyOverride: RESTRICTED_GRANULAR_POLICY,
171 },
172 web_search: {
173 prompts: [WEB_SEARCH_PROMPT],
174 defaultTranscriptFile: "fixtures/web_search/claude_transcript.ndjson",
175 queryMode: "streaming",
176 enableTools: true,
177 },
178 claude_background_subagent_after_root: {
179 prompts: [CLAUDE_BACKGROUND_SUBAGENT_AFTER_ROOT_PROMPT],
180 defaultTranscriptFile:
181 "fixtures/claude_background_subagent_after_root/claude_transcript.ndjson",
182 queryMode: "streaming",
183 enableTools: true,
184 backgroundWakeCounts: [1],
185 },
186 claude_background_task_wake: {
187 prompts: [CLAUDE_BACKGROUND_TASK_WAKE_PROMPT, CLAUDE_BACKGROUND_TASK_WAKE_FOLLOW_UP_PROMPT],
188 defaultTranscriptFile: "fixtures/claude_background_task_wake/claude_transcript.ndjson",
189 queryMode: "streaming",
190 enableTools: true,
191 backgroundWakeCounts: [1, 0],
192 },
193 claude_background_monitor_wake: {
194 prompts: [CLAUDE_BACKGROUND_MONITOR_WAKE_PROMPT],
195 defaultTranscriptFile: "fixtures/claude_background_monitor_wake/claude_transcript.ndjson",
196 queryMode: "streaming",
197 enableTools: true,
198 backgroundWakeCounts: [1],
199 },
200 claude_background_subagent_lifecycle: {
201 prompts: [
202 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_LAUNCH_PROMPT,
203 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT,
204 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_RESUME_PROMPT,
205 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_FINAL_PROMPT,
206 ],
207 defaultTranscriptFile: "fixtures/claude_background_subagent_lifecycle/claude_transcript.ndjson",
208 queryMode: "streaming",
209 enableTools: true,
210 backgroundWakeCounts: [1, 0, 1, 0],
211 },
212 claude_background_wake_before_queued_prompt: {
213 prompts: [
214 CLAUDE_BACKGROUND_WAKE_BEFORE_QUEUED_PROMPT_LAUNCH_PROMPT,
215 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_STOP_PROMPT,
216 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_RESUME_PROMPT,
217 CLAUDE_BACKGROUND_SUBAGENT_LIFECYCLE_FINAL_PROMPT,
218 ],
219 defaultTranscriptFile:
220 "fixtures/claude_background_wake_before_queued_prompt/claude_transcript.ndjson",
221 queryMode: "streaming",
222 enableTools: true,
223 backgroundWakeCounts: [1, 0, 1, 0],
224 offerNextPromptImmediately: true,
225 },
226 claude_background_task_interrupt: {
227 prompts: [CLAUDE_BACKGROUND_TASK_INTERRUPT_PROMPT],
228 defaultTranscriptFile: "fixtures/claude_background_task_interrupt/claude_transcript.ndjson",
229 queryMode: "interrupt",
230 enableTools: true,
231 interruptAfter: "tool_use",
232 interruptAfterToolUses: 2,
233 },
234 claude_nested_background_subagent_wake: {
235 prompts: [CLAUDE_NESTED_BACKGROUND_SUBAGENT_WAKE_PROMPT],
236 defaultTranscriptFile:
237 "fixtures/claude_nested_background_subagent_wake/claude_transcript.ndjson",
238 queryMode: "streaming",
239 enableTools: true,
240 backgroundWakeCounts: [1],
241 },
242 claude_nested_subagent_model: {
243 prompts: [CLAUDE_NESTED_SUBAGENT_MODEL_PROMPT],
244 defaultTranscriptFile: "fixtures/claude_nested_subagent_model/claude_transcript.ndjson",
245 queryMode: "streaming",
246 enableTools: true,
247 },
248 // Requires the recording account's claude.ai Firecrawl connector; MCP metadata uses undeclared `tool_use_meta`.
249 claude_mcp_tool_presentation: {
250 prompts: [CLAUDE_MCP_TOOL_PRESENTATION_PROMPT],
251 defaultTranscriptFile: "fixtures/claude_mcp_tool_presentation/claude_transcript.ndjson",
252 queryMode: "streaming",
253 enableTools: true,
254 },
255 subagent: {
256 prompts: [SUBAGENT_PROMPT],
257 defaultTranscriptFile: "fixtures/subagent/claude_transcript.ndjson",
258 queryMode: "streaming",
259 enableTools: true,
260 },
261 thread_rollback: {
262 prompts: [
263 THREAD_ROLLBACK_FIRST_PROMPT,
264 THREAD_ROLLBACK_SECOND_PROMPT,
265 THREAD_ROLLBACK_AFTER_PROMPT,
266 ],
267 defaultTranscriptFile: "fixtures/thread_rollback/claude_transcript.ndjson",
268 queryMode: "resume_at_cursor",
269 enableTools: true,
270 },
271 thread_fork_native: {
272 prompts: [THREAD_FORK_NATIVE_SOURCE_PROMPT, THREAD_FORK_NATIVE_TARGET_PROMPT],
273 defaultTranscriptFile: "fixtures/thread_fork_native/claude_transcript.ndjson",
274 queryMode: "fork_session",
275 enableTools: true,
276 },
277 thread_fork_native_prior_turn: {
278 prompts: [
279 THREAD_FORK_NATIVE_PRIOR_TURN_ALPHA_PROMPT,
280 THREAD_FORK_NATIVE_PRIOR_TURN_BETA_PROMPT,
281 THREAD_FORK_NATIVE_PRIOR_TURN_REPEAT_PROMPT,
282 ],
283 defaultTranscriptFile: "fixtures/thread_fork_native_prior_turn/claude_transcript.ndjson",
284 queryMode: "fork_session_prior_turn",
285 enableTools: true,
286 },
287 thread_merge_back_siblings: {
288 prompts: [
289 THREAD_MERGE_BACK_SIBLINGS_SOURCE_PROMPT,
290 THREAD_MERGE_BACK_SIBLINGS_FIRST_FORK_PROMPT,
291 THREAD_MERGE_BACK_SIBLINGS_SECOND_FORK_PROMPT,
292 THREAD_MERGE_BACK_SIBLINGS_FIRST_HANDOFF_PROMPT,
293 THREAD_MERGE_BACK_SIBLINGS_SECOND_HANDOFF_PROMPT,
294 THREAD_MERGE_BACK_SIBLINGS_RECALL_PROMPT,
295 ],
296 defaultTranscriptFile: "fixtures/thread_merge_back_siblings/claude_transcript.ndjson",
297 queryMode: "fork_session_merge_back_siblings",
298 enableTools: true,
299 },
300} as const;
301​
302function readArgValue(name: string): string | undefined {
303 const args = process.argv.slice(2);
304 const index = args.indexOf(name);
305 return index >= 0 ? args[index + 1] : undefined;
307​
308function selectedQueryMode(defaultMode: ClaudeRecordingQueryMode): ClaudeRecordingQueryMode {
309 const raw = readArgValue("--query-mode") ?? process.env.T3_CLAUDE_REPLAY_QUERY_MODE;
310 if (raw === undefined) {
311 return defaultMode;
312 }
313 if (
314 raw === "streaming" ||
315 raw === "restart" ||
316 raw === "resume_at_cursor" ||
317 raw === "fork_session" ||
318 raw === "fork_session_prior_turn" ||
319 raw === "fork_session_merge_back_siblings" ||
320 raw === "active_steering" ||
321 raw === "interrupt" ||
322 raw === "interrupt_restart"
323 ) {
324 return raw;
325 }
326 throw new Error(
327 `Unsupported Claude replay query mode '${raw}'. Use 'streaming', 'restart', 'resume_at_cursor', 'fork_session', 'fork_session_prior_turn', 'fork_session_merge_back_siblings', 'active_steering', 'interrupt', or 'interrupt_restart'.`,
328 );
330​
331const scenario = readArgValue("--scenario") ?? process.env.T3_CLAUDE_REPLAY_SCENARIO ?? "simple";
332const recording = CLAUDE_RECORDINGS[scenario as keyof typeof CLAUDE_RECORDINGS];
333const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
334​
335if (recording === undefined) {
336 throw new Error(`Claude replay fixture '${scenario}' is not configured.`);
338​
339const positionalOutputPath = process.argv[2]?.startsWith("--") ? undefined : process.argv[2];
340const path = await Effect.runPromise(
341 Effect.service(Path.Path).pipe(Effect.provide(NodeServices.layer)),
342);
343const outputPath =
344 readArgValue("--out") ??
345 positionalOutputPath ??
346 (await Effect.runPromise(
347 path.fromFileUrl(
348 new URL(
349 `../src/orchestration-v2/testkit/${recording.defaultTranscriptFile}`,
350 import.meta.url,
351 ),
352 ),
353 ));
354​
355function encodeTranscriptNdjson(
356 transcript: Awaited<ReturnType<typeof recordClaudeAgentSdkReplayTranscript>>,
357): string {
358 const { entries, ...metadata } = transcript;
359 return [
360 JSON.stringify({ type: "transcript_start", ...metadata }),
361 ...entries.map((entry) => JSON.stringify(entry)),
362 "",
363 ].join("\n");
365​
366function joinPath(directory: string, fileName: string): string {
367 return `${directory.replace(/\/+$/u, "")}/${fileName.replace(/^\/+/u, "")}`;
369​
370function runFileSystem<A, E>(effect: Effect.Effect<A, E, FileSystem.FileSystem>): Promise<A> {
371 return Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));
373​
374function selectedPrompts(): ReadonlyArray<string> {
375 if (process.env.T3_CLAUDE_REPLAY_PROMPTS !== undefined) {
376 return process.env.T3_CLAUDE_REPLAY_PROMPTS.split("\n---\n").filter(
377 (prompt) => prompt.length > 0,
378 );
379 }
380 if (process.env.T3_CLAUDE_REPLAY_PROMPT !== undefined) {
381 return [process.env.T3_CLAUDE_REPLAY_PROMPT];
382 }
383 return recording.prompts;
385​
386const prompts = selectedPrompts();
387const queryMode = selectedQueryMode(recording.queryMode);
388validateClaudeReplayRecordingSelection({
389 scenario,
390 configuredQueryMode: recording.queryMode,
391 selectedQueryMode: queryMode,
392 configuredPromptCount: recording.prompts.length,
393 selectedPromptCount: prompts.length,
394});
395​
396function runtimePolicyForRecording(input: {
397 readonly cwd: string;
398 readonly override?: RuntimePolicyV2Override;
399}): ProviderAdapter.ProviderAdapterV2RuntimePolicy {
400 return ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
401 runtimeMode: "full-access",
402 interactionMode: "default",
403 cwd: input.override?.cwd ?? input.cwd,
404 ...(input.override?.approvalPolicy === undefined
405 ? {}
406 : { approvalPolicy: input.override.approvalPolicy }),
407 ...(input.override?.sandboxPolicy === undefined
408 ? {}
409 : { sandboxPolicy: input.override.sandboxPolicy }),
410 ...(input.override?.reasoningEffort === undefined
411 ? {}
412 : { reasoningEffort: input.override.reasoningEffort }),
413 });
415​
416async function makeToolCallReadOnlyRecordingWorkspace(): Promise<string> {
417 await runFileSystem(
418 Effect.gen(function* () {
419 const fs = yield* FileSystem.FileSystem;
420 yield* fs.remove(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { recursive: true, force: true });
421 yield* fs.makeDirectory(TOOL_CALL_READ_ONLY_WORKSPACE_ROOT, { recursive: true });
422 }),
423 );
424 return TOOL_CALL_READ_ONLY_WORKSPACE_ROOT;
426​
427const cwd =
428 process.env.T3_CLAUDE_REPLAY_CWD ??
429 (scenario === "tool_call_read_only"
430 ? await makeToolCallReadOnlyRecordingWorkspace()
431 : await makeCheckpointWorkspace(`claude-agent-sdk-record-${scenario}`));
432const shouldRemoveCwd = process.env.T3_CLAUDE_REPLAY_CWD === undefined;
433​
434const expectedAbsentWorkspacePaths =
435 "expectedAbsentWorkspacePaths" in recording ? recording.expectedAbsentWorkspacePaths : [];
436​
437async function assertWorkspacePathsAbsent(phase: "before" | "after"): Promise<void> {
438 const presentPaths = await runFileSystem(
439 Effect.gen(function* () {
440 const fs = yield* FileSystem.FileSystem;
441 const present: Array<string> = [];
442 for (const relativePath of expectedAbsentWorkspacePaths) {
443 if (yield* fs.exists(joinPath(cwd, relativePath))) {
444 present.push(relativePath);
445 }
446 }
447 return present;
448 }),
449 );
450 if (presentPaths.length > 0) {
451 throw new Error(
452 `Claude replay fixture '${scenario}' expected ${presentPaths
453 .map((relativePath) => `'${relativePath}'`)
454 .join(
455 ", ",
456 )} to be absent ${phase} the recorded turn, but found in the recording workspace.`,
457 );
458 }
460​
461if (shouldRemoveCwd && (scenario === "tool_call_read_only" || scenario === "subagent")) {
462 await runFileSystem(
463 Effect.gen(function* () {
464 const fs = yield* FileSystem.FileSystem;
465 yield* fs.writeFileString(
466 joinPath(cwd, "package.json"),
467 encodeUnknownJsonString({
468 name: "claude-read-only-fixture",
469 private: true,
470 scripts: { typecheck: "tsc --noEmit" },
471 }),
472 );
473 yield* fs.writeFileString(
474 joinPath(cwd, "tsconfig.json"),
475 encodeUnknownJsonString({
476 compilerOptions: {
477 module: "ESNext",
478 strict: true,
479 target: "ES2022",
480 },
481 }),
482 );
483 }),
484 );
486​
487try {
488 const runtimePolicy = runtimePolicyForRecording({
489 cwd,
490 ...("runtimePolicyOverride" in recording ? { override: recording.runtimePolicyOverride } : {}),
491 });
492 const queryPolicy = claudeRuntimeQueryPolicyForRuntimePolicy(runtimePolicy);
493​
494 await assertWorkspacePathsAbsent("before");
495 const transcript = await recordClaudeAgentSdkReplayTranscript({
496 scenario,
497 prompts,
498 modelSelection: {
499 ...CLAUDE_MODEL_SELECTION,
500 model: process.env.T3_CLAUDE_REPLAY_MODEL ?? CLAUDE_MODEL_SELECTION.model,
501 },
502 cwd,
503 ...(process.env.T3_CLAUDE_REPLAY_SESSION_ID === undefined
504 ? {}
505 : { sessionId: process.env.T3_CLAUDE_REPLAY_SESSION_ID }),
506 queryMode,
507 ...("enableTools" in recording && recording.enableTools === true ? { enableTools: true } : {}),
508 ...(queryPolicy.tools === undefined ? {} : { tools: queryPolicy.tools }),
509 permissionMode: queryPolicy.permissionMode,
510 ...(queryPolicy.allowedTools === undefined ? {} : { allowedTools: queryPolicy.allowedTools }),
511 ...(queryPolicy.allowDangerouslySkipPermissions === undefined
512 ? {}
513 : { allowDangerouslySkipPermissions: queryPolicy.allowDangerouslySkipPermissions }),
514 ...(queryPolicy.installPermissionCallback ? { enablePermissionCallback: true } : {}),
515 ...("permissionDecision" in recording
516 ? { permissionDecision: recording.permissionDecision }
517 : {}),
518 ...("interruptAfter" in recording ? { interruptAfter: recording.interruptAfter } : {}),
519 ...("interruptAfterToolUses" in recording
520 ? { interruptAfterToolUses: recording.interruptAfterToolUses }
521 : {}),
522 ...("backgroundWakeCounts" in recording
523 ? { backgroundWakeCounts: recording.backgroundWakeCounts }
524 : {}),
525 ...("offerNextPromptImmediately" in recording
526 ? { offerNextPromptImmediately: recording.offerNextPromptImmediately }
527 : {}),
528 });
529 await assertWorkspacePathsAbsent("after");
530 const transcriptWithEvidence =
531 expectedAbsentWorkspacePaths.length === 0
532 ? transcript
533 : {
534 ...transcript,
535 metadata: {
536 ...transcript.metadata,
537 verifiedAbsentWorkspacePaths: [...expectedAbsentWorkspacePaths],
538 },
539 };
540 await runFileSystem(
541 Effect.gen(function* () {
542 const fs = yield* FileSystem.FileSystem;
543 yield* fs.makeDirectory(path.dirname(outputPath), { recursive: true });
544 yield* fs.writeFileString(outputPath, encodeTranscriptNdjson(transcriptWithEvidence));
545 }),
546 );
547 await Effect.runPromise(
548 Console.log(
549 `Wrote ${transcript.entries.length} ${CLAUDE_AGENT_SDK_REPLAY_PROTOCOL} replay entries to ${outputPath}`,
550 ),
551 );
552} finally {
553 if (shouldRemoveCwd) {
554 await runFileSystem(
555 Effect.gen(function* () {
556 const fs = yield* FileSystem.FileSystem;
557 yield* fs.remove(cwd, { recursive: true, force: true });
558 }),
559 );
560 }