Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/zod-v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@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.

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`.
2 changes: 1 addition & 1 deletion packages/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,20 @@ 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) */
cleanup_on_failure: z.boolean().optional().default(true),
/** 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({}),
});

/**
Expand Down Expand Up @@ -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 */
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/runner/runtime/sdk-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof tool>[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<Record<string, z.ZodTypeAny>>
>[3];
return tool(toolDef.name, toolDef.description, zodShape, handler);
});

Expand Down
37 changes: 23 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading