@bastani/atomic 0.5.21 → 0.5.22-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -557,18 +557,37 @@ function printDetachedBanner(tmuxSessionName: string): void {
557
557
  );
558
558
  }
559
559
 
560
- /**
561
- * Small buffer (ms) subtracted from `Date.now()` when recording the Claude
562
- * session start timestamp. Protects against fast sequential runs where
563
- * the system clock granularity could cause a just-created session's
564
- * `lastModified` to fall slightly before our recorded timestamp.
565
- */
566
- const CLAUDE_SESSION_TIMESTAMP_BUFFER_MS = 100;
567
-
568
560
  // ============================================================================
569
561
  // Session execution helpers
570
562
  // ============================================================================
571
563
 
564
+ /**
565
+ * Resolve the provider-specific session identifier for use as
566
+ * `SessionContext.sessionId`:
567
+ * - Claude interactive: `ClaudeSessionWrapper.sessionId` — the Claude UUID
568
+ * set when `createClaudeSession` ran.
569
+ * - Claude headless: `HeadlessClaudeSessionWrapper.sessionId` — the SDK
570
+ * `session_id` from the most recently completed `query()` (empty string
571
+ * until the first query returns).
572
+ * - Copilot: `CopilotSession.sessionId`.
573
+ * - OpenCode: `Session.id`.
574
+ *
575
+ * Returns an empty string for unknown shapes rather than throwing so
576
+ * early-init readers of `s.sessionId` (e.g. logging) don't crash.
577
+ */
578
+ function resolveProviderSessionId(
579
+ agent: AgentType,
580
+ providerSession: unknown,
581
+ ): string {
582
+ if (!providerSession || typeof providerSession !== "object") return "";
583
+ const obj = providerSession as Record<string, unknown>;
584
+ if (agent === "opencode") {
585
+ return typeof obj["id"] === "string" ? (obj["id"] as string) : "";
586
+ }
587
+ // claude and copilot both expose `sessionId` as a string.
588
+ return typeof obj["sessionId"] === "string" ? (obj["sessionId"] as string) : "";
589
+ }
590
+
572
591
  /** Type guard for objects with a string `content` property (Copilot assistant.message data). */
573
592
  export function hasContent(value: unknown): value is { content: string } {
574
593
  return (
@@ -579,56 +598,281 @@ export function hasContent(value: unknown): value is { content: string } {
579
598
  );
580
599
  }
581
600
 
582
- export function renderMessagesToText(messages: SavedMessage[]): string {
583
- return messages
584
- .map((m) => {
585
- switch (m.provider) {
586
- case "copilot": {
587
- if (m.data.type !== "assistant.message") return "";
588
- // SessionEvent["data"] for assistant.message has a typed `content: string`
589
- return hasContent(m.data.data) ? m.data.data.content : "";
590
- }
591
- case "opencode": {
592
- // Part is a discriminated union; filter to TextPart which has { type: "text", text: string }
593
- return m.data.parts
594
- .filter(
595
- (p): p is Extract<typeof p, { type: "text" }> =>
596
- p.type === "text",
597
- )
598
- .map((p) => p.text)
599
- .join("\n");
601
+ /**
602
+ * Character budget cap for tool-call `input` payloads embedded in the
603
+ * transcript. Tool call arguments can grow (diffs, large SQL strings, whole
604
+ * files passed inline), and the transcript's primary consumer is a
605
+ * downstream LLM that must `Read` this file as context for its own turn —
606
+ * so we cap the per-call JSON at a predictable size. The suffix
607
+ * `[+N chars]` preserves the dropped length for humans reviewing the file.
608
+ *
609
+ * Tool _results_ are intentionally NOT included in the transcript. File
610
+ * contents, shell output, and search results inflate the transcript
611
+ * dramatically and lead to context rot on the next stage. A reader (human
612
+ * or model) can still reconstruct what the tool returned by looking at
613
+ * the assistant's subsequent text — which is the whole point of the
614
+ * assistant summarising its own work.
615
+ */
616
+ const TRANSCRIPT_TOOL_INPUT_BUDGET = 800;
617
+
618
+ function truncateForTranscript(text: string, max: number): string {
619
+ if (text.length <= max) return text;
620
+ return text.slice(0, max) + ` … [+${text.length - max} chars]`;
621
+ }
622
+
623
+ /** Render a tool_use `input` object as a JSON-ish block, capped to budget. */
624
+ function renderToolInput(input: unknown): string {
625
+ let json: string;
626
+ try {
627
+ json = JSON.stringify(input, null, 2);
628
+ } catch {
629
+ json = String(input);
630
+ }
631
+ return truncateForTranscript(json, TRANSCRIPT_TOOL_INPUT_BUDGET);
632
+ }
633
+
634
+ /**
635
+ * Render a Claude transcript as readable Markdown.
636
+ *
637
+ * Captures the user/agent interaction chronologically:
638
+ * - User messages (string content) → `### User`
639
+ * - Assistant text blocks → `### Assistant`
640
+ * - Assistant `tool_use` blocks → `**→ \`Name\`**` + JSON input
641
+ *
642
+ * Intentionally omitted:
643
+ * - `tool_result` blocks — their payloads (file contents, shell output,
644
+ * stringified diffs) dominate the transcript and lead to context rot on
645
+ * the next stage. The assistant's subsequent text response already
646
+ * summarises what the tool returned; re-including the raw output
647
+ * duplicates that information at high token cost.
648
+ * - `thinking` blocks — verbose internal reasoning rarely useful when the
649
+ * transcript is re-ingested as context elsewhere.
650
+ * - `system` / `summary` / other non-message types.
651
+ */
652
+ function renderClaudeTranscript(
653
+ messages: ReadonlyArray<{ type: string; message: unknown }>,
654
+ ): string {
655
+ const sections: string[] = [];
656
+
657
+ for (const msg of messages) {
658
+ if (msg.type !== "user" && msg.type !== "assistant") continue;
659
+
660
+ // `message` shape is one of:
661
+ // - a plain string (legacy path),
662
+ // - `{ role, content: string }` (API-style plain text turn),
663
+ // - `{ role, content: Block[] }` (tool-use / tool-result turns).
664
+ // Normalise the first two into a single string; handle the third below.
665
+ const rawMessage = msg.message;
666
+ let plainText: string | null = null;
667
+ let arrayContent: unknown[] | null = null;
668
+
669
+ if (typeof rawMessage === "string") {
670
+ plainText = rawMessage;
671
+ } else if (rawMessage && typeof rawMessage === "object") {
672
+ const content = (rawMessage as { content?: unknown }).content;
673
+ if (typeof content === "string") {
674
+ plainText = content;
675
+ } else if (Array.isArray(content)) {
676
+ arrayContent = content;
677
+ }
678
+ }
679
+
680
+ if (plainText !== null) {
681
+ const trimmed = plainText.trim();
682
+ if (trimmed) {
683
+ const header = msg.type === "user" ? "### User" : "### Assistant";
684
+ sections.push(`${header}\n\n${trimmed}`);
685
+ }
686
+ continue;
687
+ }
688
+
689
+ if (arrayContent === null) continue;
690
+ const content = arrayContent;
691
+
692
+ if (msg.type === "assistant") {
693
+ // Group all blocks from a single assistant message under one header
694
+ // so text and tool calls read as one coherent turn.
695
+ const parts: string[] = [];
696
+ for (const block of content) {
697
+ if (!block || typeof block !== "object") continue;
698
+ const b = block as Record<string, unknown>;
699
+ if (b["type"] === "text" && typeof b["text"] === "string") {
700
+ const txt = (b["text"] as string).trim();
701
+ if (txt) parts.push(txt);
702
+ } else if (b["type"] === "tool_use") {
703
+ const name =
704
+ typeof b["name"] === "string" ? (b["name"] as string) : "tool";
705
+ const input = renderToolInput(b["input"]);
706
+ parts.push(`**→ \`${name}\`**\n\n\`\`\`json\n${input}\n\`\`\``);
600
707
  }
601
- case "claude": {
602
- if (m.data.type !== "assistant") return "";
603
- const msg = m.data.message;
604
- if (typeof msg === "string") return msg;
605
- if (msg && typeof msg === "object" && "content" in msg) {
606
- const { content } = msg as { content: unknown };
607
- if (typeof content === "string") return content;
608
- // Claude messages often have mixed content arrays (text +
609
- // tool_use + thinking blocks). Filter for text blocks instead
610
- // of requiring ALL blocks to be text the old isTextBlockArray
611
- // check caused a JSON.stringify fallback that embedded raw
612
- // message objects into downstream prompts.
613
- if (Array.isArray(content)) {
614
- const textParts = content
615
- .filter(
616
- (b): b is { type: "text"; text: string } =>
617
- typeof b === "object" &&
618
- b !== null &&
619
- b.type === "text" &&
620
- typeof b.text === "string",
621
- )
622
- .map((b) => b.text);
623
- if (textParts.length > 0) return textParts.join("\n");
624
- }
625
- }
626
- return "";
708
+ // Skip "thinking" blocks.
709
+ }
710
+ if (parts.length > 0) {
711
+ sections.push(`### Assistant\n\n${parts.join("\n\n")}`);
712
+ }
713
+ continue;
714
+ }
715
+
716
+ // msg.type === "user" with array content usually a batch of tool_results
717
+ // responding to the previous assistant turn's tool_use blocks. We skip
718
+ // the tool_result payloads entirely (see function docstring for why) and
719
+ // only surface any inline `text` blocks, which is where a real follow-up
720
+ // user turn would land.
721
+ for (const block of content) {
722
+ if (!block || typeof block !== "object") continue;
723
+ const b = block as Record<string, unknown>;
724
+ if (b["type"] === "text" && typeof b["text"] === "string") {
725
+ const txt = (b["text"] as string).trim();
726
+ if (txt) sections.push(`### User\n\n${txt}`);
727
+ }
728
+ }
729
+ }
730
+
731
+ return sections.join("\n\n");
732
+ }
733
+
734
+ /**
735
+ * Render a Copilot transcript as readable Markdown.
736
+ *
737
+ * Preserves the existing `assistant.message → content` extraction and adds
738
+ * `user.message` rendering plus any `toolCalls` attached to an assistant
739
+ * message. All other event types (`session.start`, `session.idle`, plain
740
+ * telemetry, etc.) are skipped.
741
+ */
742
+ function renderCopilotTranscript(
743
+ events: ReadonlyArray<{ type?: unknown; data?: unknown }>,
744
+ ): string {
745
+ const sections: string[] = [];
746
+
747
+ for (const evt of events) {
748
+ if (evt.type === "assistant.message") {
749
+ const data = evt.data;
750
+ if (!hasContent(data)) continue;
751
+ const parts: string[] = [];
752
+ const text = data.content.trim();
753
+ if (text) parts.push(text);
754
+
755
+ // toolCalls is an array on `assistant.message` data when present.
756
+ const toolCalls = (data as Record<string, unknown>)["toolCalls"];
757
+ if (Array.isArray(toolCalls)) {
758
+ for (const call of toolCalls) {
759
+ if (!call || typeof call !== "object") continue;
760
+ const c = call as Record<string, unknown>;
761
+ const name =
762
+ typeof c["name"] === "string"
763
+ ? (c["name"] as string)
764
+ : typeof c["toolName"] === "string"
765
+ ? (c["toolName"] as string)
766
+ : "tool";
767
+ const args = c["arguments"] ?? c["input"] ?? c["parameters"];
768
+ parts.push(
769
+ `**→ \`${name}\`**\n\n\`\`\`json\n${renderToolInput(args)}\n\`\`\``,
770
+ );
627
771
  }
628
772
  }
629
- })
630
- .filter((txt): txt is string => typeof txt === "string" && txt.length > 0)
631
- .join("\n\n");
773
+
774
+ if (parts.length > 0) {
775
+ sections.push(`### Assistant\n\n${parts.join("\n\n")}`);
776
+ }
777
+ continue;
778
+ }
779
+
780
+ if (evt.type === "user.message") {
781
+ const data = evt.data;
782
+ if (hasContent(data)) {
783
+ const text = data.content.trim();
784
+ if (text) sections.push(`### User\n\n${text}`);
785
+ }
786
+ }
787
+ // All other event types are intentionally skipped.
788
+ }
789
+
790
+ return sections.join("\n\n");
791
+ }
792
+
793
+ /**
794
+ * Render an OpenCode prompt response as readable Markdown.
795
+ *
796
+ * OpenCode hands us `{ info, parts }`; `parts` is a discriminated union where
797
+ * `text` parts carry the assistant reply and `tool` parts carry tool
798
+ * invocations. `reasoning` and `subtask` parts are internal and omitted.
799
+ */
800
+ function renderOpencodeTranscript(response: {
801
+ parts?: ReadonlyArray<{ type?: unknown; text?: unknown } & Record<string, unknown>>;
802
+ }): string {
803
+ if (!response.parts) return "";
804
+ const parts: string[] = [];
805
+ for (const part of response.parts) {
806
+ if (!part || typeof part !== "object") continue;
807
+ if (part.type === "text" && typeof part.text === "string") {
808
+ const txt = part.text.trim();
809
+ if (txt) parts.push(txt);
810
+ } else if (part.type === "tool") {
811
+ const name =
812
+ typeof part["tool"] === "string"
813
+ ? (part["tool"] as string)
814
+ : typeof part["name"] === "string"
815
+ ? (part["name"] as string)
816
+ : "tool";
817
+ const state = part["state"];
818
+ const args =
819
+ state && typeof state === "object"
820
+ ? (state as Record<string, unknown>)["input"] ??
821
+ (state as Record<string, unknown>)["args"]
822
+ : undefined;
823
+ parts.push(
824
+ `**→ \`${name}\`**\n\n\`\`\`json\n${renderToolInput(args)}\n\`\`\``,
825
+ );
826
+ // Tool outputs are intentionally omitted — see the comment on
827
+ // `TRANSCRIPT_TOOL_INPUT_BUDGET` for the context-rot rationale.
828
+ }
829
+ }
830
+ if (parts.length === 0) return "";
831
+ return `### Assistant\n\n${parts.join("\n\n")}`;
832
+ }
833
+
834
+ export function renderMessagesToText(messages: SavedMessage[]): string {
835
+ // Claude messages already come in as a flat chronological list — render
836
+ // the whole slice at once so the helper can cross-reference tool_use_ids
837
+ // against tool_result blocks. Copilot and OpenCode keep their existing
838
+ // per-message rendering.
839
+ const sections: string[] = [];
840
+ const claudeBatch: Array<{ type: string; message: unknown }> = [];
841
+
842
+ const flushClaude = (): void => {
843
+ if (claudeBatch.length === 0) return;
844
+ const rendered = renderClaudeTranscript(claudeBatch);
845
+ if (rendered) sections.push(rendered);
846
+ claudeBatch.length = 0;
847
+ };
848
+
849
+ for (const m of messages) {
850
+ if (m.provider === "claude") {
851
+ claudeBatch.push(
852
+ m.data as unknown as { type: string; message: unknown },
853
+ );
854
+ continue;
855
+ }
856
+ flushClaude();
857
+ if (m.provider === "copilot") {
858
+ const rendered = renderCopilotTranscript([
859
+ m.data as unknown as { type?: unknown; data?: unknown },
860
+ ]);
861
+ if (rendered) sections.push(rendered);
862
+ } else if (m.provider === "opencode") {
863
+ const rendered = renderOpencodeTranscript(
864
+ m.data as unknown as {
865
+ parts?: ReadonlyArray<
866
+ { type?: unknown; text?: unknown } & Record<string, unknown>
867
+ >;
868
+ },
869
+ );
870
+ if (rendered) sections.push(rendered);
871
+ }
872
+ }
873
+ flushClaude();
874
+
875
+ return sections.join("\n\n");
632
876
  }
633
877
 
634
878
  /** Resolve a SessionRef (string or SessionHandle) to the session name. */
@@ -946,8 +1190,6 @@ async function initProviderClientAndSession<A extends AgentType>(
946
1190
  agent: A,
947
1191
  serverUrl: string,
948
1192
  paneId: string,
949
- sessionId: string,
950
- sessionDir: string,
951
1193
  clientOpts: StageClientOptions<A>,
952
1194
  sessionOpts: StageSessionOptions<A>,
953
1195
  headless = false,
@@ -1009,16 +1251,26 @@ async function initProviderClientAndSession<A extends AgentType>(
1009
1251
  case "claude": {
1010
1252
  if (headless) {
1011
1253
  // Headless Claude stages use the Agent SDK directly — no tmux pane.
1254
+ // Each query gets its own SDK-assigned session_id; the wrapper
1255
+ // tracks the latest one and exposes it as `sessionId`.
1012
1256
  const client = new HeadlessClaudeClientWrapper();
1013
1257
  await client.start();
1014
- const session = new HeadlessClaudeSessionWrapper(sessionId);
1015
- return { client, session } as Result;
1258
+ const session = new HeadlessClaudeSessionWrapper();
1259
+ // Cast through `unknown` `HeadlessClaudeClientWrapper` intentionally
1260
+ // omits the interactive-only fields (`paneId`, `sessionDir`, etc.)
1261
+ // that `ClaudeClientWrapper` has; both satisfy the same runtime
1262
+ // contract used by workflow code.
1263
+ return { client, session } as unknown as Result;
1016
1264
  }
1017
1265
  const claudeClientOpts = clientOpts as StageClientOptions<"claude">;
1018
- const claudeSessionOpts = sessionOpts as StageSessionOptions<"claude">;
1019
- const client = new ClaudeClientWrapper(paneId, claudeClientOpts, sessionDir);
1020
- await client.start();
1021
- const session = new ClaudeSessionWrapper(paneId, sessionId, claudeSessionOpts, onHIL);
1266
+ const client = new ClaudeClientWrapper(paneId, claudeClientOpts);
1267
+ // `start()` now returns the Claude session UUID, which we pass through
1268
+ // to the session wrapper so `s.sessionId` is the Claude UUID (not the
1269
+ // atomic short ID). This fixes the parallel-workflow bug where save
1270
+ // used to look up "the newest Claude session globally" and could
1271
+ // attribute one branch's transcript to another.
1272
+ const claudeSessionId = await client.start();
1273
+ const session = new ClaudeSessionWrapper(paneId, claudeSessionId, onHIL);
1022
1274
  return { client, session } as Result;
1023
1275
  }
1024
1276
  default:
@@ -1066,7 +1318,13 @@ async function cleanupProvider<A extends AgentType>(
1066
1318
  case "claude":
1067
1319
  // Headless Claude stages have no tmux pane to clear.
1068
1320
  if (!paneId.startsWith("headless-")) {
1069
- clearClaudeSession(paneId);
1321
+ try {
1322
+ await clearClaudeSession(paneId);
1323
+ } catch (e) {
1324
+ console.warn(
1325
+ `[cleanup] claude session clear failed: ${errorMessage(e)}`,
1326
+ );
1327
+ }
1070
1328
  }
1071
1329
  break;
1072
1330
  default:
@@ -1193,49 +1451,29 @@ function createSessionRunner(
1193
1451
  const messagesPath = join(sessionDir, "messages.json");
1194
1452
  const inboxPath = join(sessionDir, "inbox.md");
1195
1453
 
1196
- // ── 11. Claude session snapshot (for identifying new sessions later) ──
1197
- let knownClaudeSessionIds: Set<string> | undefined;
1198
- if (shared.agent === "claude") {
1199
- const { listSessions } = await import("@anthropic-ai/claude-agent-sdk");
1200
- const existing = await listSessions({ dir: process.cwd() });
1201
- knownClaudeSessionIds = new Set(existing.map((s) => s.sessionId));
1202
- }
1203
- const claudeSessionStartedAfter =
1204
- shared.agent === "claude"
1205
- ? Date.now() - CLAUDE_SESSION_TIMESTAMP_BUFFER_MS
1206
- : 0;
1207
-
1208
1454
  // ── Message wrapping (Claude/Copilot/OpenCode) ──
1209
1455
  async function wrapMessages(
1210
1456
  arg: SessionEvent[] | SessionPromptResponse | string,
1211
1457
  ): Promise<SavedMessage[]> {
1212
1458
  if (typeof arg === "string") {
1213
- const { getSessionMessages, listSessions } =
1214
- await import("@anthropic-ai/claude-agent-sdk");
1215
- const dir = process.cwd();
1216
- const sessions = await listSessions({ dir });
1217
-
1218
- const newSessions = knownClaudeSessionIds
1219
- ? sessions.filter((s) => !knownClaudeSessionIds!.has(s.sessionId))
1220
- : sessions.filter(
1221
- (s) => s.lastModified >= claudeSessionStartedAfter,
1222
- );
1223
-
1224
- const candidates = newSessions.sort(
1225
- (a, b) => b.lastModified - a.lastModified,
1226
- );
1227
-
1228
- const candidate = candidates[0];
1229
- if (!candidate) {
1459
+ // `arg` is the Claude session UUID — either `s.sessionId` from an
1460
+ // interactive `ClaudeSessionWrapper` (set at `createClaudeSession`
1461
+ // time) or the SDK-emitted `session_id` tracked inside
1462
+ // `HeadlessClaudeSessionWrapper.query`. Using it directly removes
1463
+ // the "pick the globally newest Claude session" heuristic that
1464
+ // misattributed transcripts across parallel branches.
1465
+ if (!arg) {
1230
1466
  throw new Error(
1231
- `wrapMessages: no new Claude session found for ${dir}`,
1467
+ "wrapMessages: empty Claude session id. Call s.save(s.sessionId) " +
1468
+ "only after a successful s.session.query() (headless wrappers " +
1469
+ "only know their session_id once a query completes).",
1232
1470
  );
1233
1471
  }
1234
-
1235
- const msgs: SessionMessage[] = await getSessionMessages(
1236
- candidate.sessionId,
1237
- { dir },
1238
- );
1472
+ const { getSessionMessages } =
1473
+ await import("@anthropic-ai/claude-agent-sdk");
1474
+ const msgs: SessionMessage[] = await getSessionMessages(arg, {
1475
+ dir: process.cwd(),
1476
+ });
1239
1477
  return msgs.map((m) => ({ provider: "claude" as const, data: m }));
1240
1478
  }
1241
1479
 
@@ -1298,8 +1536,6 @@ function createSessionRunner(
1298
1536
  shared.agent,
1299
1537
  serverUrl,
1300
1538
  paneId,
1301
- sessionId,
1302
- sessionDir,
1303
1539
  clientOpts,
1304
1540
  sessionOpts,
1305
1541
  isHeadless,
@@ -1380,6 +1616,16 @@ function createSessionRunner(
1380
1616
  // structured workflows read their declared fields the same way.
1381
1617
  // A single uniform access pattern means workflow code never has
1382
1618
  // to branch on "is this workflow structured or free-form".
1619
+ //
1620
+ // `s.sessionId` is the provider-specific session identifier — the
1621
+ // Claude session UUID, the Copilot session id, or the OpenCode
1622
+ // session id. This is what workflows pass to `s.save(s.sessionId)`
1623
+ // to disambiguate their own transcript when several sessions run
1624
+ // in parallel under the same workflow.
1625
+ const providerSessionId = resolveProviderSessionId(
1626
+ shared.agent,
1627
+ providerSession,
1628
+ );
1383
1629
  const ctx: SessionContext = {
1384
1630
  client: providerClient,
1385
1631
  session: providerSession,
@@ -1387,7 +1633,7 @@ function createSessionRunner(
1387
1633
  agent: shared.agent,
1388
1634
  sessionDir,
1389
1635
  paneId,
1390
- sessionId,
1636
+ sessionId: providerSessionId,
1391
1637
  save,
1392
1638
  transcript: transcriptFn,
1393
1639
  getMessages: getMessagesFn,
package/src/sdk/types.ts CHANGED
@@ -22,7 +22,6 @@ import type {
22
22
  import type {
23
23
  ClaudeClientWrapper,
24
24
  ClaudeSessionWrapper,
25
- ClaudeQueryDefaults,
26
25
  } from "./providers/claude.ts";
27
26
 
28
27
  /** Supported agent types */
@@ -44,7 +43,7 @@ type ClientOptionsMap = {
44
43
  * Maps each agent to the session create options the user passes to `ctx.stage()`.
45
44
  * - OpenCode: `client.session.create()` body params
46
45
  * - Copilot: `client.createSession()` config (onPermissionRequest defaults to approveAll)
47
- * - Claude: `claudeQuery()` defaults for subsequent queries
46
+ * - Claude: no per-session options delivery is driven entirely by Stop hooks.
48
47
  */
49
48
  type SessionOptionsMap = {
50
49
  opencode: {
@@ -53,7 +52,7 @@ type SessionOptionsMap = {
53
52
  workspaceID?: string;
54
53
  };
55
54
  copilot: Partial<CopilotSessionConfig>;
56
- claude: ClaudeQueryDefaults;
55
+ claude: Record<string, never>;
57
56
  };
58
57
 
59
58
  /** Maps each agent to the `s.client` type provided in the stage callback. */
@@ -92,7 +91,6 @@ export type {
92
91
  OpencodeSession,
93
92
  ClaudeClientWrapper,
94
93
  ClaudeSessionWrapper,
95
- ClaudeQueryDefaults,
96
94
  };
97
95
 
98
96
  // ─── Validation ─────────────────────────────────────────────────────────────
@@ -35,7 +35,6 @@ export type {
35
35
  OpencodeSession,
36
36
  ClaudeClientWrapper,
37
37
  ClaudeSessionWrapper,
38
- ClaudeQueryDefaults,
39
38
  } from "../types.ts";
40
39
 
41
40
  // Re-export native SDK types for convenience