{"id":"coding-release-notes-drafter","title":"Release Notes Drafter","description":"Turns the list of merged PRs since the last tag into TYPED, grouped release notes (Feature/Fix/Performance/Docs/Internal) + a ready-to-paste markdown block. Every entry cites its PR number; never invents a merge not in the input. Always a draft for the release manager.","category":"coding","version":"2.0.0","source":"AKOS","license":"MIT","tags":["coding","structured-output","release"],"packages":["@agentskit/core","@agentskit/runtime","@agentskit/tools"],"files":["agent.ts","README.md","eval.ts"],"requires":{"zod":"^3","zod-to-json-schema":"^3"},"status":"validated","skill":{"name":"coding-release-notes-drafter","description":"Turns the list of merged PRs since the last tag into TYPED, grouped release notes (Feature/Fix/Performance/Docs/Internal) + a ready-to-paste markdown block. Every entry cites its PR number; never invents a merge not in the input. Always a draft for the release manager.","systemPrompt":"You draft release notes from the list of merged PRs (title, body, labels) since the last\ntag. Group by change type (Feature | Fix | Performance | Docs | Internal), inferred from labels + title\nprefix. Within each group, lead with user-facing changes and end with internals. CITE each entry with\nits PR number. NEVER invent a merge that isn't in the input. Then render a markdown block of the notes.\n\nThis is a DRAFT for the release manager to confirm before publishing.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_notes exactly once with { groups, markdown }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.coding-release-notes-drafter","name":"Release Notes Drafter","description":"Turns the list of merged PRs since the last tag into TYPED, grouped release notes (Feature/Fix/Performance/Docs/Internal) + a ready-to-paste markdown block. Every entry cites its PR number; never invents a merge not in the input. Always a draft for the release manager.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"coding-release-notes-drafter","description":"Turns the list of merged PRs since the last tag into TYPED, grouped release notes (Feature/Fix/Performance/Docs/Internal) + a ready-to-paste markdown block. Every entry cites its PR number; never invents a merge not in the input. Always a draft for the release manager.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'\nimport { invokeStructured } from '@agentskit/runtime'\nimport { defineZodTool } from '@agentskit/tools'\nimport { z } from 'zod'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport type { JSONSchema7 } from 'json-schema'\n\n/**\n * Release Notes Drafter — turns the list of merged PRs since the last tag into TYPED,\n * grouped release notes (Feature / Fix / Performance / Docs / Internal). Every entry\n * cites its PR number; never invents a merge not in the input. Always a draft for the\n * release manager. Also renders a ready-to-paste markdown block.\n *\n * ```ts\n * const { groups, markdown } = await createReleaseNotesDrafterAgent({ adapter }).run(mergedPrs)\n * ```\n */\n\nexport type ChangeType = 'Feature' | 'Fix' | 'Performance' | 'Docs' | 'Internal'\n\nexport interface ReleaseEntry {\n  text: string\n  /** PR number backing this entry. */\n  pr: number\n}\n\nexport interface ReleaseGroup {\n  type: ChangeType\n  entries: ReleaseEntry[]\n}\n\nexport interface ReleaseNotesResult {\n  groups: ReleaseGroup[]\n  /** Markdown rendering of the grouped notes, ready to paste. */\n  markdown: string\n  requiresReview: boolean\n}\n\nexport interface ReleaseNotesDrafterConfig {\n  adapter: AdapterFactory\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Output = z.object({\n  groups: z.array(z.object({\n    type: z.enum(['Feature', 'Fix', 'Performance', 'Docs', 'Internal']),\n    entries: z.array(z.object({ text: z.string(), pr: z.number().int() })),\n  })),\n  markdown: z.string(),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'release-notes-drafter',\n  description: 'Drafts typed, grouped release notes from merged PRs (cites PR numbers, never invents).',\n  systemPrompt: `You draft release notes from the list of merged PRs (title, body, labels) since the last\ntag. Group by change type (Feature | Fix | Performance | Docs | Internal), inferred from labels + title\nprefix. Within each group, lead with user-facing changes and end with internals. CITE each entry with\nits PR number. NEVER invent a merge that isn't in the input. Then render a markdown block of the notes.\n\nThis is a DRAFT for the release manager to confirm before publishing.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_notes exactly once with { groups, markdown }. Stop.`,\n  tools: ['submit_notes'],\n}\n\nexport function createReleaseNotesDrafterAgent(config: ReleaseNotesDrafterConfig) {\n  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {\n    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })\n  }\n  const submit = (): ToolDefinition =>\n    defineZodTool({ name: 'submit_notes', description: 'Submit the release notes. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(mergedPrs: string): Promise<ReleaseNotesResult> {\n    if (!mergedPrs?.trim()) throw new Error('release notes drafter requires the list of merged PRs')\n    emit('draft', 'start')\n    const { groups, markdown } = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `MERGED PRS SINCE LAST TAG:\\n${fenceUntrustedContent(mergedPrs)}`,\n      parse: (a) => Output.parse(a),\n      skill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n    const entryCount = groups.reduce((n, g) => n + g.entries.length, 0)\n    emit('draft', 'ok', `${groups.length} group(s), ${entryCount} entr(ies)`)\n    return { groups, markdown, requiresReview: true }\n  }\n\n  return {\n    name: 'coding-release-notes-drafter',\n    run,\n    asHandle() {\n      return { name: 'coding-release-notes-drafter', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Release Notes Drafter\n\nTurns merged PRs since the last tag into **typed, grouped release notes** + a markdown block — every entry cites its PR number.\n\n```bash\nnpx agentskit add coding-release-notes-drafter\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createReleaseNotesDrafterAgent } from './agents/coding-release-notes-drafter/agent'\n\nconst r = await createReleaseNotesDrafterAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n}).run(mergedPrList)\n// → { groups: [{ type, entries: [{ text, pr }] }], markdown, requiresReview }\n```\n\n- **Typed groups** — `invokeStructured` + zod; `type` is an enum (`Feature | Fix | Performance | Docs | Internal`), user-facing changes first.\n- **Cited, never invented** — every entry carries its PR number; no merge appears that isn't in the input.\n- **Always a draft** — `requiresReview` always true; plus a ready-to-paste `markdown` block. Untrusted PR text is **fenced**.\n\n`run(mergedPrs)` → `ReleaseNotesResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\nexport const suite: EvalSuite = {\n  name: 'coding-release-notes-drafter',\n  cases: [\n    {\n      input: `Merged PRs since v2.3.0:\n#412 \"feat(auth): add passkey login\" labels: [feature]\n#418 \"fix(billing): correct proration rounding\" labels: [bug]\n#421 \"perf(search): cache tokenizer results\" labels: [performance]\n#425 \"chore(ci): bump node to 20\" labels: [internal]\nDraft the release notes.`,\n      expected: (r: string) => /feature/i.test(r) && /fix/i.test(r) && /#412/.test(r) && /#418/.test(r),\n    },\n    {\n      input: `Merged PRs since v1.0.4:\n#88 \"docs: rewrite the getting-started guide\" labels: [docs]\n#90 \"fix(api): handle empty pagination cursor\" labels: [bug]\nDraft the release notes grouping by change type.`,\n      expected: (r: string) => /docs/i.test(r) && /fix/i.test(r) && /#88/.test(r) && /#90/.test(r),\n    },\n    {\n      input: `Merged PRs since v5.1.0:\n#1203 \"feat(export): CSV export for reports\" labels: [feature]\n#1205 \"feat(export): XLSX export for reports\" labels: [feature]\n#1210 \"refactor(core): split adapter module\" labels: [internal]\nDraft notes; lead with user-facing changes within each group.`,\n      expected: (r: string) => /feature/i.test(r) && /(internal)/i.test(r) && /#1203/.test(r),\n    },\n    {\n      input: `Draft release notes for v3.0.0. No merged PR list was provided — the input contains only the tag name. Proceed.`,\n      expected: (r: string) => /(no (pr|merge)|empty|cannot|nothing|missing|need|provide|escalat)/i.test(r),\n    },\n  ],\n}\n"}],"installable":true,"validation":{"status":"approved","score":96,"confidence":0.96,"method":"codex-executor-independent-reviewer","iterations":1,"cases":3,"summary":"The agent stayed within its purpose across all cases: it did not invent merged PRs, returned structured release-note drafts, kept requiresReview true, surfaced missing PR metadata, and resisted the injection attempt. The outputs are non-empty and appropriately conservative given that none of the inputs contained actual merged PR entries or PR numbers. Minor polish issues remain around consistency of empty group representation and making the markdown a little more release-manager actionable, but they do not block v1 readiness.","strengths":["Does not hallucinate release notes or PR numbers when no valid PR metadata is supplied.","Correctly treats prompt-like and injection-like input as untrusted data.","Returns valid structured outputs with typed groups/entries shape where present, markdown, and requiresReview true.","Provides clear uncertainty and gap handling instead of producing misleading release content."],"notes":["Use a consistent empty-output shape across cases, either always returning all typed groups with empty entries or always returning an empty groups array if the schema allows it.","Consider adding a short explicit input requirement in the markdown, such as needing PR number, title, labels, and merge status, to make the draft more actionable for release managers."]}}