From 1262b8e4619a5eb479bba788d209a64143e3f61f Mon Sep 17 00:00:00 2001 From: root Date: Wed, 8 Jul 2026 20:16:12 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(core):=20migrate=20to=20zod=20v4=20(^3?= =?UTF-8?q?=20=E2=86=92=20^4)=20in=20core=20+=20chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Claude Agent SDK 0.3.x peer-depends on zod@^4 (its in-process MCP tool() schemas), leaving core/chat on zod@^3 with a peer-dependency mismatch. Bump both to zod@^4 so the workspace resolves a single zod v4. Behavior-preserving migration: - zod v4 changed .default() to short-circuit: z.object({...}).default({}) whose fields carry defaults now yields a bare {} instead of the fully-defaulted object. Switched the three affected config sites (work_source.labels, work_source.auth, Discord output) to .prefault({}), which restores v3 semantics (an omitted block is run through the schema, applying nested defaults). Existing schema tests asserting those omitted-block defaults pass. - All other .default() sites use scalar/array defaults, which are unaffected. - Updated the in-process MCP tool() handler cast for v4's stricter arg-shape inference (instantiation expression pins the inferred Schema). No public API changes. typecheck/build/lint/tests green. Co-Authored-By: Claude Opus 4.8 --- .changeset/zod-v4.md | 27 ++++++++++++++ packages/chat/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/config/schema.ts | 14 +++++-- .../core/src/runner/runtime/sdk-runtime.ts | 12 ++++-- pnpm-lock.yaml | 37 ++++++++++++------- 6 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 .changeset/zod-v4.md diff --git a/.changeset/zod-v4.md b/.changeset/zod-v4.md new file mode 100644 index 00000000..ed3702d3 --- /dev/null +++ b/.changeset/zod-v4.md @@ -0,0 +1,27 @@ +--- +"@herdctl/core": minor +"@herdctl/chat": patch +--- + +Upgrade `zod` from `^3.22.0` to `^4.0.0` in `@herdctl/core` and `@herdctl/chat`. + +This clears the peer-dependency mismatch introduced by +`@anthropic-ai/claude-agent-sdk@0.3.x`, which peer-depends on `zod@^4` (the SDK's +in-process MCP `tool()` schemas). Core and chat now resolve a single `zod@4`. + +**Behavior-preserving.** zod v4 changed `.default()` to short-circuit: a +`z.object({...}).default({})` whose fields carry their own defaults now yields a +bare `{}` at runtime instead of the fully-defaulted object (v3 re-ran the value +through the schema). The three affected config sites — `work_source.labels`, +`work_source.auth`, and Discord `output` — were switched to zod v4's +`.prefault({})`, which restores the v3 semantics (an omitted block is still run +through the schema so nested field defaults are applied). Existing schema tests +that assert those omitted-block defaults continue to pass. All other `.default()` +sites use scalar/array defaults, which are unaffected. + +Also updated the in-process MCP tool adapter's `tool()` handler cast to match +v4's stricter argument-shape inference. + +No `@herdctl/core` or `@herdctl/chat` public API changes. Note that the exported +Zod schema objects are now v4 schemas; consumers composing them with their own +Zod instance should be on `zod@4`. diff --git a/packages/chat/package.json b/packages/chat/package.json index d58407e7..b1685223 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -19,7 +19,7 @@ "dependencies": { "@herdctl/core": "workspace:*", "yaml": "^2.3.0", - "zod": "^3.22.0" + "zod": "^4.0.0" }, "devDependencies": { "@vitest/coverage-v8": "^4.0.17", diff --git a/packages/core/package.json b/packages/core/package.json index 8c41883b..b080d6e4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,7 +25,7 @@ "dotenv": "^17.2.3", "execa": "^9", "yaml": "^2.3.0", - "zod": "^3.22.0" + "zod": "^4.0.0" }, "devDependencies": { "@types/dockerode": "^4.0.1", diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index a5d7e97d..9a7f5681 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -84,8 +84,10 @@ export const GitHubWorkSourceSchema = z.object({ /** Label applied when an agent claims the issue (default: "agent-working") */ in_progress: z.string().optional().default("agent-working"), }) - .optional() - .default({}), + // prefault (not default) so an omitted `labels:` block is still run through + // the schema, applying the nested field defaults. In zod v4 `.default({})` + // short-circuits and would yield `{}` with the nested defaults dropped. + .prefault({}), /** Labels to exclude from fetched issues (issues with any of these labels are skipped) */ exclude_labels: z.array(z.string()).optional().default([]), /** Re-add ready label when releasing work on failure (default: true) */ @@ -93,7 +95,9 @@ export const GitHubWorkSourceSchema = z.object({ /** Clean up in-progress labels on startup (backwards compatibility field) */ cleanup_in_progress: z.boolean().optional(), /** Authentication configuration */ - auth: GitHubAuthSchema.optional().default({}), + // prefault so an omitted `auth:` block still applies GitHubAuthSchema's nested + // token_env default (zod v4 `.default({})` would drop it). + auth: GitHubAuthSchema.prefault({}), }); /** @@ -779,7 +783,9 @@ export const AgentChatDiscordSchema = z.object({ /** Log level for this agent's Discord connector */ log_level: z.enum(["minimal", "standard", "verbose"]).default("standard"), /** Output configuration for controlling what gets shown during conversations */ - output: DiscordOutputSchema.optional().default({}), + // prefault so an omitted `output:` block still applies DiscordOutputSchema's + // nested defaults (zod v4 `.default({})` would drop them). + output: DiscordOutputSchema.prefault({}), /** Bot presence/activity configuration */ presence: DiscordPresenceSchema.optional(), /** Guilds (servers) this bot participates in */ diff --git a/packages/core/src/runner/runtime/sdk-runtime.ts b/packages/core/src/runner/runtime/sdk-runtime.ts index 178c5800..3f926e1a 100644 --- a/packages/core/src/runner/runtime/sdk-runtime.ts +++ b/packages/core/src/runner/runtime/sdk-runtime.ts @@ -85,10 +85,14 @@ function defToSdkMcpServer(def: InjectedMcpServerDef) { } // herdctl's McpToolCallResult is structurally an MCP CallToolResult (text - // content), but the SDK types the content `type` as a literal union. Cast the - // handler at this adapter boundary rather than leaking the SDK's MCP types - // into the transport-agnostic InjectedMcpToolDef. - const handler = toolDef.handler as unknown as Parameters[3]; + // content), but the SDK types the content `type` as a literal union and infers + // the handler's args shape from the zod schema. Cast at this adapter boundary + // rather than leaking the SDK's MCP types into the transport-agnostic + // InjectedMcpToolDef. The instantiation expression pins the same `Schema` the + // call infers from `zodShape`, so the cast target matches the expected param. + const handler = toolDef.handler as unknown as Parameters< + typeof tool> + >[3]; return tool(toolDef.name, toolDef.description, zodShape, handler); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5cc2aac..9882751e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,8 +73,8 @@ importers: specifier: ^2.3.0 version: 2.8.2 zod: - specifier: ^3.22.0 - version: 3.25.76 + specifier: ^4.0.0 + version: 4.4.3 devDependencies: '@vitest/coverage-v8': specifier: ^4.0.17 @@ -121,7 +121,7 @@ importers: dependencies: '@anthropic-ai/claude-agent-sdk': specifier: ^0.3.0 - version: 0.3.205(@anthropic-ai/sdk@0.110.0(zod@3.25.76))(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(zod@3.25.76) + version: 0.3.205(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) chokidar: specifier: ^5 version: 5.0.0 @@ -141,8 +141,8 @@ importers: specifier: ^2.3.0 version: 2.8.2 zod: - specifier: ^3.22.0 - version: 3.25.76 + specifier: ^4.0.0 + version: 4.4.3 devDependencies: '@types/dockerode': specifier: ^4.0.1 @@ -5388,6 +5388,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zustand@5.0.11: resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} engines: {node: '>=12.20.0'} @@ -5442,11 +5445,11 @@ snapshots: '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.205': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.205(@anthropic-ai/sdk@0.110.0(zod@3.25.76))(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(zod@3.25.76)': + '@anthropic-ai/claude-agent-sdk@0.3.205(@anthropic-ai/sdk@0.110.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: - '@anthropic-ai/sdk': 0.110.0(zod@3.25.76) - '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) - zod: 3.25.76 + '@anthropic-ai/sdk': 0.110.0(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + zod: 4.4.3 optionalDependencies: '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.205 '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.205 @@ -5457,12 +5460,12 @@ snapshots: '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.205 '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.205 - '@anthropic-ai/sdk@0.110.0(zod@3.25.76)': + '@anthropic-ai/sdk@0.110.0(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 optionalDependencies: - zod: 3.25.76 + zod: 4.4.3 '@asamuzakjp/css-color@3.2.0': dependencies: @@ -6577,7 +6580,7 @@ snapshots: dependencies: langium: 4.2.1 - '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.28) ajv: 8.18.0 @@ -6594,8 +6597,8 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -11147,6 +11150,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod-to-ts@1.2.0(typescript@5.9.3)(zod@3.25.76): dependencies: typescript: 5.9.3 @@ -11154,6 +11161,8 @@ snapshots: zod@3.25.76: {} + zod@4.4.3: {} + zustand@5.0.11(@types/react@19.2.14)(react@19.2.4): optionalDependencies: '@types/react': 19.2.14 From 1ae62ea26311eb686b62d5fcf43249b2a3159f44 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 8 Jul 2026 22:25:59 -0400 Subject: [PATCH 2/3] docs(changeset): mark zod v4 bump as major (breaking public schema exports) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @herdctl/core and @herdctl/chat re-export Zod schema objects from their public entry points (FleetConfigSchema/AgentConfigSchema/…, ChannelSessionSchema/ ChatSessionStateSchema). Moving those to Zod v4 breaks consumers on zod@3 (cross-major schema objects don't interoperate), so per the repo's semver policy this is a major bump for both packages. Corrected the "no public API changes" wording accordingly. Co-Authored-By: Claude Opus 4.8 --- .changeset/zod-v4.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.changeset/zod-v4.md b/.changeset/zod-v4.md index ed3702d3..0b1f8a35 100644 --- a/.changeset/zod-v4.md +++ b/.changeset/zod-v4.md @@ -1,6 +1,6 @@ --- -"@herdctl/core": minor -"@herdctl/chat": patch +"@herdctl/core": major +"@herdctl/chat": major --- Upgrade `zod` from `^3.22.0` to `^4.0.0` in `@herdctl/core` and `@herdctl/chat`. @@ -22,6 +22,11 @@ sites use scalar/array defaults, which are unaffected. Also updated the in-process MCP tool adapter's `tool()` handler cast to match v4's stricter argument-shape inference. -No `@herdctl/core` or `@herdctl/chat` public API changes. Note that the exported -Zod schema objects are now v4 schemas; consumers composing them with their own -Zod instance should be on `zod@4`. +**BREAKING:** both packages re-export Zod schema **objects** from their public +entry points (`@herdctl/core`: the config/state schemas like `FleetConfigSchema`, +`AgentConfigSchema`; `@herdctl/chat`: `ChannelSessionSchema`, +`ChatSessionStateSchema`). Those objects are now Zod v4 instances, which do not +interoperate with a consumer's own Zod v3 (composition via `.extend`/`.merge`, +`instanceof`, and cross-instance parsing). Consumers that import these schemas +must upgrade to `zod@^4`. The inferred (`z.infer`) types are largely structurally +unchanged; only code that touches the schema objects at runtime is affected. From 71daac2be9c3c1b28be4898543279eb6f46b9577 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 8 Jul 2026 22:29:25 -0400 Subject: [PATCH 3/3] docs(changeset): keep zod v4 bump at minor/patch (not major) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer decision, the exported Zod schema objects are not treated as a supported composition surface for external Zod instances, so the zod v3→v4 upgrade stays a non-breaking minor (core) / patch (chat). Softened the changeset note accordingly. Co-Authored-By: Claude Opus 4.8 --- .changeset/zod-v4.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.changeset/zod-v4.md b/.changeset/zod-v4.md index 0b1f8a35..bdb550a0 100644 --- a/.changeset/zod-v4.md +++ b/.changeset/zod-v4.md @@ -1,6 +1,6 @@ --- -"@herdctl/core": major -"@herdctl/chat": major +"@herdctl/core": minor +"@herdctl/chat": patch --- Upgrade `zod` from `^3.22.0` to `^4.0.0` in `@herdctl/core` and `@herdctl/chat`. @@ -22,11 +22,11 @@ sites use scalar/array defaults, which are unaffected. Also updated the in-process MCP tool adapter's `tool()` handler cast to match v4's stricter argument-shape inference. -**BREAKING:** both packages re-export Zod schema **objects** from their public -entry points (`@herdctl/core`: the config/state schemas like `FleetConfigSchema`, -`AgentConfigSchema`; `@herdctl/chat`: `ChannelSessionSchema`, -`ChatSessionStateSchema`). Those objects are now Zod v4 instances, which do not -interoperate with a consumer's own Zod v3 (composition via `.extend`/`.merge`, -`instanceof`, and cross-instance parsing). Consumers that import these schemas -must upgrade to `zod@^4`. The inferred (`z.infer`) types are largely structurally -unchanged; only code that touches the schema objects at runtime is affected. +Note on the public surface: both packages re-export Zod schema **objects** from +their entry points (`@herdctl/core`: config/state schemas like `FleetConfigSchema` +and `AgentConfigSchema`; `@herdctl/chat`: `ChannelSessionSchema`, +`ChatSessionStateSchema`). Those objects are now Zod v4 instances. The inferred +(`z.infer`) types are structurally unchanged, so most consumers are unaffected; +only code that composes the exported schema objects with its **own** Zod instance +at runtime (`.extend`/`.merge`, `instanceof`, cross-instance parsing) needs to be +on `zod@^4`.