Skip to content

feat(core): migrate to zod v4 (core + chat)#305

Merged
edspencer merged 3 commits into
mainfrom
feat/zod-v4
Jul 9, 2026
Merged

feat(core): migrate to zod v4 (core + chat)#305
edspencer merged 3 commits into
mainfrom
feat/zod-v4

Conversation

@edspencer

@edspencer edspencer commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Upgrades zod from ^3.22.0^4.0.0 in @herdctl/core and @herdctl/chat,
clearing the peer-dependency mismatch introduced by
@anthropic-ai/claude-agent-sdk@0.3.x (which peer-depends on zod@^4 for its
in-process MCP tool() schemas). After this, the workspace resolves a single
zod@4.

Stacked on #304. This PR targets feat/bump-claude-agent-sdk-0.3 so its
diff is zod-only and CI verifies SDK 0.3.x + zod 4 together. Once #304
merges, GitHub will retarget this PR's base to main automatically (or I'll
retarget it).

The one real risk: a silent zod v4 behavior change

zod v4 changed .default() to short-circuitz.object({…}).default({})
whose fields carry their own defaults now yields a bare {} at runtime instead
of the fully-defaulted object (v3 re-ran the default value through the schema).
A naive bump would therefore silently drop nested config defaults.

Confirmed empirically:

pattern parse(undefined) in v4
.optional().default({}) {} — nested defaults dropped ❌
.prefault({}) { …nested defaults applied }

Only three config sites are object .default({}) on schemas with nested
defaults; all three were switched to zod v4's .prefault({}) to preserve v3
behavior:

  • work_source.labels (nested ready / in_progress)
  • work_source.auth (nested token_env)
  • Discord output (many nested defaults)

All other .default() sites are scalar/array defaults and are unaffected
(fleet-state's .default({}) sites have no nested defaults, so they stay).

Also updated the in-process MCP tool() handler cast for v4's stricter
argument-shape inference (an instantiation expression pins the same Schema the
call infers from the zod shape).

Verification

  • pnpm typecheck11/11 pass
  • pnpm build7/7 pass
  • pnpm lint6/6 pass
  • Core tests — 3190 pass (only the pre-existing root-only directory.test.ts writable-check fails locally; passes in CI as non-root)
  • Chat tests — 277 pass
  • The nested-default behavior is guarded by pre-existing tests that assert the omitted-block outcome — they pass with .prefault and would fail with a naive .default({}):
    • schema.test.ts "applies default labels when not specified"
    • schema.test.ts "defaults auth.token_env to GITHUB_TOKEN" / "accepts empty auth object (uses defaults)"
    • schema.test.ts "applies output defaults when output is omitted"

Notes

  • No @herdctl/core / @herdctl/chat public API changes. The exported Zod schema objects are now v4 schemas, so consumers composing them with their own Zod instance should be on zod@4.
  • Changeset added (@herdctl/core: minor, @herdctl/chat: patch).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Upgraded Zod support to version 4 across core and chat packages, aligning with newer dependency requirements.
  • Bug Fixes

    • Preserved existing config behavior when optional nested settings are omitted, so defaults continue to apply as expected.
    • Improved compatibility for tool handling with stricter Zod v4 typing.

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 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Upgrades the zod dependency from ^3.22.0 to ^4.0.0 in @herdctl/core and @herdctl/chat. Adjusts config schema defaults (.prefault({})) to preserve prior nested-default behavior, and updates a type cast for the MCP tool handler to match zod v4's stricter type inference.

Changes

Zod v4 dependency upgrade and compatibility fixes

Layer / File(s) Summary
Dependency version bump
packages/core/package.json, packages/chat/package.json, .changeset/zod-v4.md
zod dependency bumped from ^3.22.0 to ^4.0.0 in both packages; changeset documents the peer-dependency fix and migration notes.
Config schema default preservation
packages/core/src/config/schema.ts
GitHubWorkSourceSchema (labels, auth) and AgentChatDiscordSchema (output) switched from .optional().default({}) to .prefault({}) so omitted nested blocks still apply nested field defaults.
MCP tool handler type cast update
packages/core/src/runner/runtime/sdk-runtime.ts
The toolDef.handler cast in defToSdkMcpServer() now parameterizes tool with Record<string, z.ZodTypeAny> to align with zod v4's stricter argument-shape inference; runtime behavior unchanged.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: migrating core and chat to Zod v4.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/zod-v4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploying herdctl with  Cloudflare Pages  Cloudflare Pages

Latest commit: 71daac2
Status: ✅  Deploy successful!
Preview URL: https://95dbced3.herdctl.pages.dev
Branch Preview URL: https://feat-zod-v4.herdctl.pages.dev

View logs

@edspencer

Copy link
Copy Markdown
Owner Author

ℹ️ CI note (stacked PR)

The repo's CI workflow only triggers on push/pull_request to main:

on:
  push: { branches: [main] }
  pull_request: { branches: [main] }

Because this PR is stacked on feat/bump-claude-agent-sdk-0.3 (base ≠ main), the 5 core jobs (Lint / Typecheck / Build / Unit / Web UI) don't run yet. They'll trigger automatically once #304 merges and GitHub retargets this PR's base to main.

Suggested merge order: merge #304 first, then this.

In the meantime I ran the equivalent jobs locally against the combined SDK 0.3.x + zod 4 tree:

  • pnpm typecheck — 11/11 ✅
  • pnpm build — 7/7 ✅
  • pnpm lint — 6/6 ✅
  • Core tests — 3190 pass ✅ (only the pre-existing root-only directory.test.ts writable-check fails locally; passes in CI as non-root)
  • Chat tests — 277 pass ✅

@edspencer

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/zod-v4.md:
- Around line 2-3: Update the changeset entry to mark both `@herdctl/core` and
`@herdctl/chat` as major instead of minor/patch, since the public schema
re-exports from their entry points are a breaking Zod v4 change. Also remove the
“No public API changes” wording from the changeset message so it reflects the
API impact accurately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dca8ffb7-44a5-4baf-95e0-dad6bce6b6f7

📥 Commits

Reviewing files that changed from the base of the PR and between 4eec7f3 and 1262b8e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • .changeset/zod-v4.md
  • packages/chat/package.json
  • packages/core/package.json
  • packages/core/src/config/schema.ts
  • packages/core/src/runner/runtime/sdk-runtime.ts

Comment thread .changeset/zod-v4.md
root and others added 2 commits July 8, 2026 22:25
…ports)

@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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@edspencer edspencer changed the base branch from feat/bump-claude-agent-sdk-0.3 to main July 9, 2026 02:30
@edspencer edspencer closed this Jul 9, 2026
@edspencer edspencer reopened this Jul 9, 2026
@edspencer edspencer merged commit 58be504 into main Jul 9, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant