@oh-my-pi/pi-coding-agent 15.0.2 → 15.1.1

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.
Files changed (138) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/examples/custom-tools/README.md +11 -7
  3. package/examples/custom-tools/hello/index.ts +2 -2
  4. package/examples/extensions/README.md +19 -8
  5. package/examples/extensions/api-demo.ts +15 -19
  6. package/examples/extensions/hello.ts +5 -6
  7. package/examples/extensions/plan-mode.ts +1 -1
  8. package/examples/extensions/reload-runtime.ts +4 -3
  9. package/examples/extensions/with-deps/index.ts +4 -3
  10. package/examples/sdk/06-extensions.ts +4 -2
  11. package/package.json +7 -17
  12. package/src/autoresearch/tools/init-experiment.ts +38 -41
  13. package/src/autoresearch/tools/log-experiment.ts +32 -41
  14. package/src/autoresearch/tools/run-experiment.ts +3 -3
  15. package/src/autoresearch/tools/update-notes.ts +11 -11
  16. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  17. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  18. package/src/commit/agentic/tools/git-hunk.ts +5 -5
  19. package/src/commit/agentic/tools/git-overview.ts +4 -4
  20. package/src/commit/agentic/tools/propose-changelog.ts +13 -13
  21. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  22. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  23. package/src/commit/agentic/tools/schemas.ts +28 -28
  24. package/src/commit/agentic/tools/split-commit.ts +22 -21
  25. package/src/commit/analysis/summary.ts +4 -4
  26. package/src/commit/changelog/generate.ts +7 -11
  27. package/src/commit/shared-llm.ts +22 -34
  28. package/src/config/config-file.ts +35 -13
  29. package/src/config/model-registry.ts +9 -190
  30. package/src/config/models-config-schema.ts +166 -0
  31. package/src/config/settings-schema.ts +18 -0
  32. package/src/edit/index.ts +2 -2
  33. package/src/edit/modes/apply-patch.ts +7 -6
  34. package/src/edit/modes/patch.ts +18 -25
  35. package/src/edit/modes/replace.ts +18 -20
  36. package/src/eval/js/shared/rewrite-imports.ts +131 -10
  37. package/src/eval/py/executor.ts +233 -623
  38. package/src/eval/py/kernel.ts +27 -2
  39. package/src/exa/factory.ts +5 -4
  40. package/src/exa/mcp-client.ts +1 -1
  41. package/src/exa/researcher.ts +9 -20
  42. package/src/exa/search.ts +26 -52
  43. package/src/exa/types.ts +1 -1
  44. package/src/exa/websets.ts +54 -53
  45. package/src/exec/bash-executor.ts +2 -1
  46. package/src/extensibility/custom-commands/loader.ts +5 -3
  47. package/src/extensibility/custom-commands/types.ts +4 -2
  48. package/src/extensibility/custom-tools/loader.ts +5 -3
  49. package/src/extensibility/custom-tools/types.ts +7 -6
  50. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  51. package/src/extensibility/extensions/loader.ts +7 -3
  52. package/src/extensibility/extensions/types.ts +9 -5
  53. package/src/extensibility/extensions/wrapper.ts +1 -2
  54. package/src/extensibility/hooks/loader.ts +3 -1
  55. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  56. package/src/extensibility/hooks/types.ts +4 -2
  57. package/src/extensibility/plugins/legacy-pi-compat.ts +30 -0
  58. package/src/extensibility/shared-events.ts +1 -1
  59. package/src/extensibility/typebox.ts +391 -0
  60. package/src/goals/tools/goal-tool.ts +6 -12
  61. package/src/hashline/types.ts +4 -4
  62. package/src/hindsight/state.ts +2 -2
  63. package/src/index.ts +0 -2
  64. package/src/internal-urls/docs-index.generated.ts +7 -7
  65. package/src/lsp/types.ts +30 -38
  66. package/src/mcp/manager.ts +1 -1
  67. package/src/mcp/tool-bridge.ts +1 -1
  68. package/src/modes/components/session-observer-overlay.ts +12 -1
  69. package/src/modes/components/status-line/segments.ts +2 -1
  70. package/src/modes/controllers/command-controller.ts +27 -2
  71. package/src/modes/controllers/event-controller.ts +3 -4
  72. package/src/modes/interactive-mode.ts +1 -1
  73. package/src/modes/rpc/host-tools.ts +1 -1
  74. package/src/modes/rpc/rpc-client.ts +1 -1
  75. package/src/modes/rpc/rpc-types.ts +1 -1
  76. package/src/modes/theme/theme.ts +111 -117
  77. package/src/modes/types.ts +1 -1
  78. package/src/modes/utils/context-usage.ts +2 -2
  79. package/src/sdk.ts +31 -8
  80. package/src/session/agent-session.ts +74 -104
  81. package/src/session/messages.ts +16 -51
  82. package/src/session/session-manager.ts +22 -2
  83. package/src/session/streaming-output.ts +16 -6
  84. package/src/task/executor.ts +208 -86
  85. package/src/task/index.ts +15 -11
  86. package/src/task/render.ts +32 -5
  87. package/src/task/types.ts +54 -39
  88. package/src/tools/ask.ts +12 -12
  89. package/src/tools/ast-edit.ts +11 -15
  90. package/src/tools/ast-grep.ts +9 -10
  91. package/src/tools/bash.ts +9 -23
  92. package/src/tools/browser.ts +39 -53
  93. package/src/tools/calculator.ts +12 -11
  94. package/src/tools/checkpoint.ts +7 -7
  95. package/src/tools/debug.ts +40 -43
  96. package/src/tools/eval.ts +6 -8
  97. package/src/tools/find.ts +10 -13
  98. package/src/tools/gh.ts +71 -128
  99. package/src/tools/hindsight-recall.ts +4 -6
  100. package/src/tools/hindsight-reflect.ts +5 -5
  101. package/src/tools/hindsight-retain.ts +15 -17
  102. package/src/tools/image-gen.ts +32 -82
  103. package/src/tools/index.ts +4 -1
  104. package/src/tools/inspect-image.ts +8 -9
  105. package/src/tools/irc.ts +15 -27
  106. package/src/tools/job.ts +14 -21
  107. package/src/tools/read.ts +7 -8
  108. package/src/tools/recipe/index.ts +7 -9
  109. package/src/tools/render-mermaid.ts +12 -12
  110. package/src/tools/report-tool-issue.ts +4 -4
  111. package/src/tools/resolve.ts +11 -11
  112. package/src/tools/review.ts +14 -26
  113. package/src/tools/search-tool-bm25.ts +7 -9
  114. package/src/tools/search.ts +19 -22
  115. package/src/tools/ssh.ts +7 -7
  116. package/src/tools/todo-write.ts +26 -34
  117. package/src/tools/vim.ts +10 -26
  118. package/src/tools/write.ts +5 -5
  119. package/src/tools/yield.ts +100 -54
  120. package/src/web/search/index.ts +9 -24
  121. package/src/prompts/compaction/branch-summary-context.md +0 -5
  122. package/src/prompts/compaction/branch-summary-preamble.md +0 -2
  123. package/src/prompts/compaction/branch-summary.md +0 -30
  124. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  125. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  126. package/src/prompts/compaction/compaction-summary.md +0 -38
  127. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  128. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  129. package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
  130. package/src/prompts/system/file-operations.md +0 -10
  131. package/src/prompts/system/handoff-document.md +0 -49
  132. package/src/prompts/system/summarization-system.md +0 -3
  133. package/src/session/compaction/branch-summarization.ts +0 -324
  134. package/src/session/compaction/compaction.ts +0 -1420
  135. package/src/session/compaction/errors.ts +0 -31
  136. package/src/session/compaction/index.ts +0 -8
  137. package/src/session/compaction/pruning.ts +0 -91
  138. package/src/session/compaction/utils.ts +0 -184
@@ -1,5 +1,5 @@
1
1
  import { Text } from "@oh-my-pi/pi-tui";
2
- import { Type } from "@sinclair/typebox";
2
+ import * as z from "zod/v4";
3
3
  import type { ToolDefinition } from "../../extensibility/extensions";
4
4
  import type { Theme } from "../../modes/theme/theme";
5
5
  import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
@@ -8,16 +8,16 @@ import { buildExperimentState } from "../state";
8
8
  import { openAutoresearchStorageIfExists } from "../storage";
9
9
  import type { AutoresearchToolFactoryOptions } from "../types";
10
10
 
11
- const updateNotesSchema = Type.Object({
12
- body: Type.String({
13
- description: "Replacement markdown body for the active autoresearch session's notes (your durable playbook).",
14
- }),
15
- append_idea: Type.Optional(
16
- Type.String({
17
- description:
18
- "When set, append this string as a new bullet under an Ideas section instead of replacing the body. `body` is ignored.",
19
- }),
20
- ),
11
+ const updateNotesSchema = z.object({
12
+ body: z
13
+ .string()
14
+ .describe("Replacement markdown body for the active autoresearch session's notes (your durable playbook)."),
15
+ append_idea: z
16
+ .string()
17
+ .describe(
18
+ "When set, append this string as a new bullet under an Ideas section instead of replacing the body. `body` is ignored.",
19
+ )
20
+ .optional(),
21
21
  });
22
22
 
23
23
  interface UpdateNotesDetails {
@@ -1,5 +1,5 @@
1
1
  import { prompt } from "@oh-my-pi/pi-utils";
2
- import { Type } from "@sinclair/typebox";
2
+ import * as z from "zod/v4";
3
3
  import analyzeFilePrompt from "../../../commit/agentic/prompts/analyze-file.md" with { type: "text" };
4
4
  import type { CommitAgentState } from "../../../commit/agentic/state";
5
5
  import type { NumstatEntry } from "../../../commit/types";
@@ -12,9 +12,9 @@ import type { TaskParams } from "../../../task/types";
12
12
  import type { ToolSession } from "../../../tools";
13
13
  import { getFilePriority } from "./git-file-diff";
14
14
 
15
- const analyzeFileSchema = Type.Object({
16
- files: Type.Array(Type.String({ description: "File path" }), { minItems: 1 }),
17
- goal: Type.Optional(Type.String({ description: "Optional analysis focus" })),
15
+ const analyzeFileSchema = z.object({
16
+ files: z.array(z.string().describe("File path")).min(1),
17
+ goal: z.string().describe("Optional analysis focus").optional(),
18
18
  });
19
19
 
20
20
  const analyzeFileOutputSchema = {
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
4
  import * as git from "../../../utils/git";
@@ -131,9 +131,9 @@ function processDiffs(files: string[], diffs: Map<string, string>): { result: st
131
131
  return { result: parts.join("\n\n"), truncatedFiles };
132
132
  }
133
133
 
134
- const gitFileDiffSchema = Type.Object({
135
- files: Type.Array(Type.String({ description: "Files to diff" }), { minItems: 1, maxItems: 10 }),
136
- staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
134
+ const gitFileDiffSchema = z.object({
135
+ files: z.array(z.string().describe("Files to diff")).min(1).max(10),
136
+ staged: z.boolean().describe("Use staged changes (default: true)").optional(),
137
137
  });
138
138
 
139
139
  export function createGitFileDiffTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitFileDiffSchema> {
@@ -1,12 +1,12 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
  import type { DiffHunk, FileHunks } from "../../../commit/types";
3
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
4
  import * as git from "../../../utils/git";
5
5
 
6
- const gitHunkSchema = Type.Object({
7
- file: Type.String({ description: "File path" }),
8
- hunks: Type.Optional(Type.Array(Type.Number({ description: "1-based hunk indices" }), { minItems: 1 })),
9
- staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
6
+ const gitHunkSchema = z.object({
7
+ file: z.string().describe("File path"),
8
+ hunks: z.array(z.number().describe("1-based hunk indices")).min(1).optional(),
9
+ staged: z.boolean().describe("Use staged changes (default: true)").optional(),
10
10
  });
11
11
 
12
12
  function selectHunks(fileHunks: FileHunks, requested?: number[]): DiffHunk[] {
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
  import type { CommitAgentState, GitOverviewSnapshot } from "../../../commit/agentic/state";
3
3
  import { extractScopeCandidates } from "../../../commit/analysis/scope";
4
4
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
@@ -42,9 +42,9 @@ function filterExcludedFiles(files: string[]): { filtered: string[]; excluded: s
42
42
  return { filtered, excluded };
43
43
  }
44
44
 
45
- const gitOverviewSchema = Type.Object({
46
- staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
47
- include_untracked: Type.Optional(Type.Boolean({ description: "Include untracked files when staged=false" })),
45
+ const gitOverviewSchema = z.object({
46
+ staged: z.boolean().describe("Use staged changes (default: true)").optional(),
47
+ include_untracked: z.boolean().describe("Include untracked files when staged=false").optional(),
48
48
  });
49
49
 
50
50
  export function createGitOverviewTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitOverviewSchema> {
@@ -1,29 +1,29 @@
1
- import { type TSchema, Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
3
  import { CHANGELOG_CATEGORIES, type ChangelogCategory } from "../../../commit/types";
4
4
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
5
5
 
6
- const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, TSchema>>(
6
+ const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, z.ZodType>>(
7
7
  (acc, category) => {
8
- acc[category] = Type.Optional(Type.Array(Type.String()));
8
+ acc[category] = z.array(z.string()).optional();
9
9
  return acc;
10
10
  },
11
- {} as Record<ChangelogCategory, TSchema>,
11
+ {} as Record<ChangelogCategory, z.ZodType>,
12
12
  );
13
13
 
14
- const changelogEntriesSchema = Type.Object(changelogEntryProperties);
15
- const changelogDeletionsSchema = Type.Object(changelogEntryProperties, {
16
- description: "Entries to remove from existing changelog sections (case-insensitive match)",
17
- });
14
+ const changelogEntriesSchema = z.object(changelogEntryProperties);
15
+ const changelogDeletionsSchema = z
16
+ .object(changelogEntryProperties)
17
+ .describe("Entries to remove from existing changelog sections (case-insensitive match)");
18
18
 
19
- const changelogEntrySchema = Type.Object({
20
- path: Type.String(),
19
+ const changelogEntrySchema = z.object({
20
+ path: z.string(),
21
21
  entries: changelogEntriesSchema,
22
- deletions: Type.Optional(changelogDeletionsSchema),
22
+ deletions: changelogDeletionsSchema.optional(),
23
23
  });
24
24
 
25
- const proposeChangelogSchema = Type.Object({
26
- entries: Type.Array(changelogEntrySchema),
25
+ const proposeChangelogSchema = z.object({
26
+ entries: z.array(changelogEntrySchema),
27
27
  });
28
28
 
29
29
  interface ChangelogResponse {
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
3
  import {
4
4
  capDetails,
@@ -15,12 +15,12 @@ import type { CustomTool } from "../../../extensibility/custom-tools/types";
15
15
  import * as git from "../../../utils/git";
16
16
  import { commitTypeSchema, detailSchema } from "./schemas.js";
17
17
 
18
- const proposeCommitSchema = Type.Object({
18
+ const proposeCommitSchema = z.object({
19
19
  type: commitTypeSchema,
20
- scope: Type.Union([Type.String(), Type.Null()]),
21
- summary: Type.String(),
22
- details: Type.Array(detailSchema),
23
- issue_refs: Type.Array(Type.String()),
20
+ scope: z.union([z.string(), z.null()]),
21
+ summary: z.string(),
22
+ details: z.array(detailSchema),
23
+ issue_refs: z.array(z.string()),
24
24
  });
25
25
 
26
26
  interface ProposalResponse {
@@ -1,9 +1,9 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
3
3
  import * as git from "../../../utils/git";
4
4
 
5
- const recentCommitsSchema = Type.Object({
6
- count: Type.Optional(Type.Number({ description: "Number of commits to fetch", minimum: 1, maximum: 50 })),
5
+ const recentCommitsSchema = z.object({
6
+ count: z.number().min(1).max(50).describe("Number of commits to fetch").optional(),
7
7
  });
8
8
 
9
9
  interface RecentCommitStats {
@@ -1,31 +1,31 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
 
3
- export const commitTypeSchema = Type.Union([
4
- Type.Literal("feat"),
5
- Type.Literal("fix"),
6
- Type.Literal("refactor"),
7
- Type.Literal("perf"),
8
- Type.Literal("docs"),
9
- Type.Literal("test"),
10
- Type.Literal("build"),
11
- Type.Literal("ci"),
12
- Type.Literal("chore"),
13
- Type.Literal("style"),
14
- Type.Literal("revert"),
15
- ]);
3
+ export const commitTypeSchema = z.enum([
4
+ "feat",
5
+ "fix",
6
+ "refactor",
7
+ "perf",
8
+ "docs",
9
+ "test",
10
+ "build",
11
+ "ci",
12
+ "chore",
13
+ "style",
14
+ "revert",
15
+ ] as const);
16
16
 
17
- export const detailSchema = Type.Object({
18
- text: Type.String(),
19
- changelog_category: Type.Optional(
20
- Type.Union([
21
- Type.Literal("Added"),
22
- Type.Literal("Changed"),
23
- Type.Literal("Fixed"),
24
- Type.Literal("Deprecated"),
25
- Type.Literal("Removed"),
26
- Type.Literal("Security"),
27
- Type.Literal("Breaking Changes"),
28
- ]),
29
- ),
30
- user_visible: Type.Optional(Type.Boolean()),
17
+ export const detailSchema = z.object({
18
+ text: z.string(),
19
+ changelog_category: z
20
+ .union([
21
+ z.literal("Added"),
22
+ z.literal("Changed"),
23
+ z.literal("Fixed"),
24
+ z.literal("Deprecated"),
25
+ z.literal("Removed"),
26
+ z.literal("Security"),
27
+ z.literal("Breaking Changes"),
28
+ ])
29
+ .optional(),
30
+ user_visible: z.boolean().optional(),
31
31
  });
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox";
1
+ import * as z from "zod/v4";
2
2
  import type { CommitAgentState, SplitCommitGroup, SplitCommitPlan } from "../../../commit/agentic/state";
3
3
  import { computeDependencyOrder } from "../../../commit/agentic/topo-sort";
4
4
  import {
@@ -15,31 +15,32 @@ import type { CustomTool } from "../../../extensibility/custom-tools/types";
15
15
  import * as git from "../../../utils/git";
16
16
  import { commitTypeSchema, detailSchema } from "./schemas.js";
17
17
 
18
- const hunkSelectorSchema = Type.Union([
19
- Type.Object({ type: Type.Literal("all") }),
20
- Type.Object({ type: Type.Literal("indices"), indices: Type.Array(Type.Number(), { minItems: 1 }) }),
21
- Type.Object({ type: Type.Literal("lines"), start: Type.Number(), end: Type.Number() }),
18
+ const hunkSelectorSchema = z.discriminatedUnion("type", [
19
+ z.object({ type: z.literal("all") }),
20
+ z.object({ type: z.literal("indices"), indices: z.array(z.number()).min(1) }),
21
+ z.object({ type: z.literal("lines"), start: z.number(), end: z.number() }),
22
22
  ]);
23
23
 
24
- const fileChangeSchema = Type.Object({
25
- path: Type.String(),
24
+ const fileChangeSchema = z.object({
25
+ path: z.string(),
26
26
  hunks: hunkSelectorSchema,
27
27
  });
28
28
 
29
- const splitCommitSchema = Type.Object({
30
- commits: Type.Array(
31
- Type.Object({
32
- changes: Type.Array(fileChangeSchema, { minItems: 1 }),
33
- type: commitTypeSchema,
34
- scope: Type.Union([Type.String(), Type.Null()]),
35
- summary: Type.String(),
36
- details: Type.Optional(Type.Array(detailSchema)),
37
- issue_refs: Type.Optional(Type.Array(Type.String())),
38
- rationale: Type.Optional(Type.String()),
39
- dependencies: Type.Optional(Type.Array(Type.Number())),
40
- }),
41
- { minItems: 2 },
42
- ),
29
+ const splitCommitSchema = z.object({
30
+ commits: z
31
+ .array(
32
+ z.object({
33
+ changes: z.array(fileChangeSchema).min(1),
34
+ type: commitTypeSchema,
35
+ scope: z.union([z.string(), z.null()]),
36
+ summary: z.string(),
37
+ details: z.array(detailSchema).optional(),
38
+ issue_refs: z.array(z.string()).optional(),
39
+ rationale: z.string().optional(),
40
+ dependencies: z.array(z.number()).optional(),
41
+ }),
42
+ )
43
+ .min(2),
43
44
  });
44
45
 
45
46
  interface SplitCommitResponse {
@@ -2,7 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
6
6
  import summarySystemPrompt from "../../commit/prompts/summary-system.md" with { type: "text" };
7
7
  import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
8
8
  import type { CommitSummary } from "../../commit/types";
@@ -12,8 +12,8 @@ import { extractTextContent, extractToolCall } from "../utils";
12
12
  const SummaryTool = {
13
13
  name: "create_commit_summary",
14
14
  description: "Generate the summary line for a conventional commit message.",
15
- parameters: Type.Object({
16
- summary: Type.String(),
15
+ parameters: z.object({
16
+ summary: z.string(),
17
17
  }),
18
18
  };
19
19
 
@@ -83,7 +83,7 @@ function renderSummaryPrompt({
83
83
  function parseSummaryFromResponse(message: AssistantMessage, commitType: string, scope: string | null): CommitSummary {
84
84
  const toolCall = extractToolCall(message, "create_commit_summary");
85
85
  if (toolCall) {
86
- const parsed = validateToolCall([SummaryTool], toolCall) as { summary: string };
86
+ const parsed = validateToolCall([SummaryTool], toolCall) as z.infer<(typeof SummaryTool)["parameters"]>;
87
87
  return { summary: stripTypePrefix(parsed.summary, commitType, scope) };
88
88
  }
89
89
  const text = extractTextContent(message);
@@ -2,27 +2,23 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { type TSchema, Type } from "@sinclair/typebox";
5
+ import * as z from "zod/v4";
6
6
  import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" };
7
7
  import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
8
8
  import { CHANGELOG_CATEGORIES, type ChangelogCategory, type ChangelogGenerationResult } from "../../commit/types";
9
9
  import { toReasoningEffort } from "../../thinking";
10
10
  import { extractTextContent, extractToolCall, parseJsonPayload } from "../utils";
11
11
 
12
- const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, TSchema>>(
13
- (acc, category) => {
14
- acc[category] = Type.Optional(Type.Array(Type.String()));
15
- return acc;
16
- },
17
- {} as Record<ChangelogCategory, TSchema>,
18
- );
12
+ const changelogEntryShape = Object.fromEntries(
13
+ CHANGELOG_CATEGORIES.map(c => [c, z.array(z.string()).optional()] as const),
14
+ ) as Record<ChangelogCategory, z.ZodOptional<z.ZodArray<z.ZodString>>>;
19
15
 
20
- const changelogEntriesSchema = Type.Object(changelogEntryProperties);
16
+ const changelogEntriesSchema = z.object(changelogEntryShape);
21
17
 
22
18
  export const changelogTool = {
23
19
  name: "create_changelog_entries",
24
20
  description: "Generate changelog entries grouped by Keep a Changelog categories.",
25
- parameters: Type.Object({
21
+ parameters: z.object({
26
22
  entries: changelogEntriesSchema,
27
23
  }),
28
24
  };
@@ -72,7 +68,7 @@ export async function generateChangelogEntries({
72
68
  function parseChangelogResponse(message: AssistantMessage): ChangelogGenerationResult {
73
69
  const toolCall = extractToolCall(message, "create_changelog_entries");
74
70
  if (toolCall) {
75
- const parsed = validateToolCall([changelogTool], toolCall) as ChangelogGenerationResult;
71
+ const parsed = validateToolCall([changelogTool], toolCall) as z.infer<(typeof changelogTool)["parameters"]>;
76
72
  return { entries: parsed.entries ?? {} };
77
73
  }
78
74
 
@@ -1,48 +1,36 @@
1
1
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { validateToolCall } from "@oh-my-pi/pi-ai";
3
- import { Type } from "@sinclair/typebox";
3
+ import * as z from "zod/v4";
4
4
  import type { ChangelogCategory, ConventionalAnalysis } from "./types";
5
5
  import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "./utils";
6
6
 
7
+ const changelogCategoryLiteral = z.enum([
8
+ "Added",
9
+ "Changed",
10
+ "Fixed",
11
+ "Deprecated",
12
+ "Removed",
13
+ "Security",
14
+ "Breaking Changes",
15
+ ]);
16
+
7
17
  /**
8
- * Shared TypeBox schema for the `create_conventional_analysis` tool used by
18
+ * Shared Zod schema for the `create_conventional_analysis` tool used by
9
19
  * both the single-pass analysis call and the map-reduce reduce phase. Schemas
10
20
  * are identical across phases — only the surrounding tool `description`
11
21
  * differs to reflect the input the phase is summarizing.
12
22
  */
13
- export const conventionalAnalysisParameters = Type.Object({
14
- type: Type.Union([
15
- Type.Literal("feat"),
16
- Type.Literal("fix"),
17
- Type.Literal("refactor"),
18
- Type.Literal("docs"),
19
- Type.Literal("test"),
20
- Type.Literal("chore"),
21
- Type.Literal("style"),
22
- Type.Literal("perf"),
23
- Type.Literal("build"),
24
- Type.Literal("ci"),
25
- Type.Literal("revert"),
26
- ]),
27
- scope: Type.Union([Type.String(), Type.Null()]),
28
- details: Type.Array(
29
- Type.Object({
30
- text: Type.String(),
31
- changelog_category: Type.Optional(
32
- Type.Union([
33
- Type.Literal("Added"),
34
- Type.Literal("Changed"),
35
- Type.Literal("Fixed"),
36
- Type.Literal("Deprecated"),
37
- Type.Literal("Removed"),
38
- Type.Literal("Security"),
39
- Type.Literal("Breaking Changes"),
40
- ]),
41
- ),
42
- user_visible: Type.Optional(Type.Boolean()),
23
+ export const conventionalAnalysisParameters = z.object({
24
+ type: z.enum(["feat", "fix", "refactor", "docs", "test", "chore", "style", "perf", "build", "ci", "revert"]),
25
+ scope: z.union([z.string(), z.null()]),
26
+ details: z.array(
27
+ z.object({
28
+ text: z.string(),
29
+ changelog_category: changelogCategoryLiteral.optional(),
30
+ user_visible: z.boolean().optional(),
43
31
  }),
44
32
  ),
45
- issue_refs: Type.Array(Type.String()),
33
+ issue_refs: z.array(z.string()),
46
34
  });
47
35
 
48
36
  export interface ConventionalAnalysisTool {
@@ -80,7 +68,7 @@ export function parseConventionalAnalysisResponse(
80
68
  ): ConventionalAnalysis {
81
69
  const toolCall = extractToolCall(message, tool.name);
82
70
  if (toolCall) {
83
- const parsed = validateToolCall([tool], toolCall) as ParsedConventionalAnalysis;
71
+ const parsed = validateToolCall([tool], toolCall) as z.infer<typeof conventionalAnalysisParameters>;
84
72
  return normalizeAnalysis(parsed);
85
73
  }
86
74
  const text = extractTextContent(message);
@@ -1,10 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
4
- import type { TSchema } from "@sinclair/typebox";
5
- import { Value } from "@sinclair/typebox/value";
6
- import type { ErrorObject } from "ajv";
7
4
  import { JSONC, YAML } from "bun";
5
+ import type { ZodType } from "zod/v4";
6
+
7
+ /** Minimal subset of the AJV ConfigSchemaError shape this module actually relies on. */
8
+ interface ConfigSchemaError {
9
+ instancePath: string;
10
+ message: string | undefined;
11
+ }
8
12
 
9
13
  function migrateJsonToYml(jsonPath: string, ymlPath: string) {
10
14
  try {
@@ -25,7 +29,7 @@ function migrateJsonToYml(jsonPath: string, ymlPath: string) {
25
29
 
26
30
  export interface IConfigFile<T> {
27
31
  readonly id: string;
28
- readonly schema: TSchema;
32
+ readonly schema: ZodType<T>;
29
33
  path?(): string;
30
34
  load(): T | null;
31
35
  invalidate?(): void;
@@ -35,7 +39,7 @@ export class ConfigError extends Error {
35
39
  readonly #message: string;
36
40
  constructor(
37
41
  public readonly id: string,
38
- public readonly schemaErrors: ErrorObject[] | null | undefined,
42
+ public readonly schemaErrors: ConfigSchemaError[] | null | undefined,
39
43
  public readonly other?: { err: unknown; stage: string },
40
44
  ) {
41
45
  let messages: string[] | undefined;
@@ -68,7 +72,6 @@ export class ConfigError extends Error {
68
72
  break;
69
73
  default:
70
74
  message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
71
- break;
72
75
  }
73
76
 
74
77
  super(message, { cause });
@@ -99,7 +102,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
99
102
 
100
103
  constructor(
101
104
  readonly id: string,
102
- readonly schema: TSchema,
105
+ readonly schema: ZodType<T>,
103
106
  configPath: string = path.join(getAgentDir(), `${id}.yml`),
104
107
  ) {
105
108
  this.#basePath = configPath;
@@ -146,7 +149,14 @@ export class ConfigFile<T> implements IConfigFile<T> {
146
149
  }
147
150
 
148
151
  createDefault(): T {
149
- return Value.Default(this.schema, [], undefined) as T;
152
+ const parsed = this.schema.safeParse({});
153
+ if (parsed.success) return parsed.data;
154
+ const fallback = this.schema.safeParse(undefined);
155
+ if (fallback.success) return fallback.data;
156
+ throw new ConfigError(this.id, undefined, {
157
+ err: new Error("Schema produced no default value"),
158
+ stage: "createDefault",
159
+ });
150
160
  }
151
161
 
152
162
  #storeCache(result: LoadResult<T>): LoadResult<T> {
@@ -169,17 +179,29 @@ export class ConfigFile<T> implements IConfigFile<T> {
169
179
  throw new Error(`Invalid config file path: ${this.#basePath}`);
170
180
  }
171
181
 
172
- if (!Value.Check(this.schema, parsed)) {
173
- const schemaErrors: ErrorObject[] = [];
174
- for (const err of Value.Errors(this.schema, parsed)) {
175
- schemaErrors.push({ instancePath: err.path, message: err.message } as ErrorObject);
182
+ const checked = this.schema.safeParse(parsed);
183
+ if (!checked.success) {
184
+ const schemaErrors: ConfigSchemaError[] = [];
185
+ for (const issue of checked.error.issues) {
186
+ const instancePath = issue.path.length === 0 ? "" : `/${issue.path.map(String).join("/")}`;
187
+ schemaErrors.push({ instancePath, message: issue.message });
176
188
  if (schemaErrors.length >= 50) break;
177
189
  }
178
190
  const error = new ConfigError(this.id, schemaErrors);
179
191
  logger.warn("Failed to parse config file", { path: this.path(), error });
180
192
  return this.#storeCache({ error, status: "error" });
181
193
  }
182
- return this.#storeCache({ value: parsed as T, status: "ok" });
194
+ const value = checked.data;
195
+ try {
196
+ this.#auxValidate?.(value);
197
+ } catch (error) {
198
+ const wrapped =
199
+ error instanceof ConfigError
200
+ ? error
201
+ : new ConfigError(this.id, undefined, { err: error, stage: "AuxValidate" });
202
+ return this.#storeCache({ error: wrapped, status: "error" });
203
+ }
204
+ return this.#storeCache({ value, status: "ok" });
183
205
  } catch (error) {
184
206
  if (isEnoent(error)) {
185
207
  return this.#storeCache({ status: "not-found" });