diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 757ed3138..09302f653 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -3,7 +3,7 @@ ## [Unreleased] ### Fixed - +- Fixed changelog tools to enforce category-specific arrays and reuse the shared category list for generation - Non-interactive environment variables (pager, editor, prompt suppression) were not applied to non-PTY bash execution, causing commands to potentially block on pagers or prompts ### Changed diff --git a/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts b/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts index 0e9a8f6f6..9b5cfaf03 100644 --- a/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts +++ b/packages/coding-agent/src/commit/agentic/tools/propose-changelog.ts @@ -1,16 +1,25 @@ -import { Type } from "@sinclair/typebox"; +import { type TSchema, Type } from "@sinclair/typebox"; import type { CommitAgentState } from "../../../commit/agentic/state"; -import type { ChangelogCategory } from "../../../commit/types"; +import { CHANGELOG_CATEGORIES, type ChangelogCategory } from "../../../commit/types"; import type { CustomTool } from "../../../extensibility/custom-tools/types"; +const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce>( + (acc, category) => { + acc[category] = Type.Optional(Type.Array(Type.String())); + return acc; + }, + {} as Record, +); + +const changelogEntriesSchema = Type.Object(changelogEntryProperties); +const changelogDeletionsSchema = Type.Object(changelogEntryProperties, { + description: "Entries to remove from existing changelog sections (case-insensitive match)", +}); + const changelogEntrySchema = Type.Object({ path: Type.String(), - entries: Type.Record(Type.String(), Type.Array(Type.String())), - deletions: Type.Optional( - Type.Record(Type.String(), Type.Array(Type.String()), { - description: "Entries to remove from existing changelog sections (case-insensitive match)", - }), - ), + entries: changelogEntriesSchema, + deletions: Type.Optional(changelogDeletionsSchema), }); const proposeChangelogSchema = Type.Object({ @@ -23,15 +32,7 @@ interface ChangelogResponse { warnings: string[]; } -const allowedCategories = new Set([ - "Breaking Changes", - "Added", - "Changed", - "Deprecated", - "Removed", - "Fixed", - "Security", -]); +const allowedCategories = new Set(CHANGELOG_CATEGORIES); export function createProposeChangelogTool( state: CommitAgentState, @@ -50,11 +51,16 @@ export function createProposeChangelogTool( const normalized = params.entries.map(entry => { const cleaned: Record = {}; - for (const [category, values] of Object.entries(entry.entries ?? {})) { + const entries = entry.entries as Record; + for (const [category, values] of Object.entries(entries)) { if (!allowedCategories.has(category as ChangelogCategory)) { errors.push(`Unknown changelog category for ${entry.path}: ${category}`); continue; } + if (!Array.isArray(values)) { + errors.push(`Invalid changelog entries for ${entry.path}: ${category}`); + continue; + } const items = values.map(value => value.trim().replace(/\.$/, "")).filter(value => value.length > 0); if (items.length > 0) { cleaned[category] = Array.from(new Set(items)); @@ -64,11 +70,16 @@ export function createProposeChangelogTool( let cleanedDeletions: Record | undefined; if (entry.deletions) { cleanedDeletions = {}; - for (const [category, values] of Object.entries(entry.deletions)) { + const deletions = entry.deletions as Record; + for (const [category, values] of Object.entries(deletions)) { if (!allowedCategories.has(category as ChangelogCategory)) { errors.push(`Unknown deletion category for ${entry.path}: ${category}`); continue; } + if (!Array.isArray(values)) { + errors.push(`Invalid deletion entries for ${entry.path}: ${category}`); + continue; + } const items = values.map(value => value.trim()).filter(value => value.length > 0); if (items.length > 0) { cleanedDeletions[category] = Array.from(new Set(items)); diff --git a/packages/coding-agent/src/commit/changelog/generate.ts b/packages/coding-agent/src/commit/changelog/generate.ts index cb86b097f..fdb07515a 100644 --- a/packages/coding-agent/src/commit/changelog/generate.ts +++ b/packages/coding-agent/src/commit/changelog/generate.ts @@ -1,17 +1,27 @@ import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai"; import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai"; -import { Type } from "@sinclair/typebox"; +import { type TSchema, Type } from "@sinclair/typebox"; import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" }; import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" }; -import type { ChangelogGenerationResult } from "../../commit/types"; +import { CHANGELOG_CATEGORIES, type ChangelogCategory, type ChangelogGenerationResult } from "../../commit/types"; import { renderPromptTemplate } from "../../config/prompt-templates"; import { extractTextContent, extractToolCall, parseJsonPayload } from "../utils"; -const ChangelogTool = { +const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce>( + (acc, category) => { + acc[category] = Type.Optional(Type.Array(Type.String())); + return acc; + }, + {} as Record, +); + +const changelogEntriesSchema = Type.Object(changelogEntryProperties); + +export const changelogTool = { name: "create_changelog_entries", description: "Generate changelog entries grouped by Keep a Changelog categories.", parameters: Type.Object({ - entries: Type.Record(Type.String(), Type.Array(Type.String())), + entries: changelogEntriesSchema, }), }; @@ -46,7 +56,7 @@ export async function generateChangelogEntries({ { systemPrompt: renderPromptTemplate(changelogSystemPrompt), messages: [{ role: "user", content: prompt, timestamp: Date.now() }], - tools: [ChangelogTool], + tools: [changelogTool], }, { apiKey, maxTokens: 1200 }, ); @@ -58,7 +68,7 @@ export async function generateChangelogEntries({ function parseChangelogResponse(message: AssistantMessage): ChangelogGenerationResult { const toolCall = extractToolCall(message, "create_changelog_entries"); if (toolCall) { - const parsed = validateToolCall([ChangelogTool], toolCall) as ChangelogGenerationResult; + const parsed = validateToolCall([changelogTool], toolCall) as ChangelogGenerationResult; return { entries: parsed.entries ?? {} }; } diff --git a/packages/coding-agent/src/commit/changelog/index.ts b/packages/coding-agent/src/commit/changelog/index.ts index 62a97b7cc..3a35f0f97 100644 --- a/packages/coding-agent/src/commit/changelog/index.ts +++ b/packages/coding-agent/src/commit/changelog/index.ts @@ -2,11 +2,12 @@ import * as path from "node:path"; import type { Api, Model } from "@oh-my-pi/pi-ai"; import { logger } from "@oh-my-pi/pi-utils"; import type { ControlledGit } from "../../commit/git"; +import { CHANGELOG_CATEGORIES } from "../../commit/types"; import { detectChangelogBoundaries } from "./detect"; import { generateChangelogEntries } from "./generate"; import { parseUnreleasedSection } from "./parse"; -const CHANGELOG_SECTIONS = ["Breaking Changes", "Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]; +const CHANGELOG_SECTIONS = CHANGELOG_CATEGORIES; const DEFAULT_MAX_DIFF_CHARS = 120_000; diff --git a/packages/coding-agent/src/commit/types.ts b/packages/coding-agent/src/commit/types.ts index e59c43409..7aba6cefc 100644 --- a/packages/coding-agent/src/commit/types.ts +++ b/packages/coding-agent/src/commit/types.ts @@ -1,7 +1,6 @@ /** * Types for the omp commit pipeline. */ - export type CommitType = | "feat" | "fix" @@ -24,6 +23,16 @@ export type ChangelogCategory = | "Fixed" | "Security"; +export const CHANGELOG_CATEGORIES: ChangelogCategory[] = [ + "Breaking Changes", + "Added", + "Changed", + "Deprecated", + "Removed", + "Fixed", + "Security", +]; + export interface CommitCommandArgs { /** Push after commit */ push: boolean;