{"id":"coding-qa-author","title":"QA Author","description":"Turns a PRD's acceptance criteria into TYPED Vitest spec stubs (one+ describe/it block per criterion, each referencing its number). Returns { specs: [{ path, body, criteria }] } and flags uncovered criteria rather than silently dropping coverage.","category":"coding","version":"2.0.0","source":"AKOS","license":"MIT","tags":["coding","structured-output","testing"],"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-qa-author","description":"Turns a PRD's acceptance criteria into TYPED Vitest spec stubs (one+ describe/it block per criterion, each referencing its number). Returns { specs: [{ path, body, criteria }] } and flags uncovered criteria rather than silently dropping coverage.","systemPrompt":"You write Vitest test suites for a TypeScript monorepo. For EACH acceptance criterion in\nthe input PRD, produce one or more spec stubs using describe and it blocks that read like executable\ndocumentation. Each it block must reference the criterion by number, contain a meaningful assertion\n(or a clear assertion comment), and compile without error. Match the project's file-naming pattern.\n\nReturn specs as { path, body, criteria: [criterionNumbers] }. Do not skip a criterion silently.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_specs exactly once with { specs }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.coding-qa-author","name":"QA Author","description":"Turns a PRD's acceptance criteria into TYPED Vitest spec stubs (one+ describe/it block per criterion, each referencing its number). Returns { specs: [{ path, body, criteria }] } and flags uncovered criteria rather than silently dropping coverage.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"coding-qa-author","description":"Turns a PRD's acceptance criteria into TYPED Vitest spec stubs (one+ describe/it block per criterion, each referencing its number). Returns { specs: [{ path, body, criteria }] } and flags uncovered criteria rather than silently dropping coverage.","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 * QA Author — turns a PRD's acceptance criteria into TYPED Vitest spec stubs (one or more\n * `describe`/`it` blocks per criterion, each referencing its criterion number). Returns\n * `{ specs: [{ path, body }] }` so a downstream agent can write the files verbatim, and\n * flags any criterion that produced no spec rather than silently dropping coverage.\n *\n * ```ts\n * const { specs, uncovered } = await createQaAuthorAgent({ adapter }).run(prdJson)\n * ```\n */\n\nexport interface SpecFile {\n  path: string\n  body: string\n  /** Criterion number(s) this spec covers. */\n  criteria: number[]\n}\n\nexport interface QaResult {\n  specs: SpecFile[]\n  /** Criterion numbers no spec covered — review before relying on the suite. */\n  uncovered: number[]\n  requiresReview: boolean\n}\n\nexport interface QaAuthorConfig {\n  adapter: AdapterFactory\n  /** Total criteria in the PRD, used to compute `uncovered`. Optional. */\n  criteriaCount?: number\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Output = z.object({\n  specs: z.array(z.object({\n    path: z.string(),\n    body: z.string(),\n    criteria: z.array(z.number().int()),\n  })),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'qa-author',\n  description: 'Turns PRD acceptance criteria into typed Vitest spec stubs (one+ per criterion).',\n  systemPrompt: `You write Vitest test suites for a TypeScript monorepo. For EACH acceptance criterion in\nthe input PRD, produce one or more spec stubs using describe and it blocks that read like executable\ndocumentation. Each it block must reference the criterion by number, contain a meaningful assertion\n(or a clear assertion comment), and compile without error. Match the project's file-naming pattern.\n\nReturn specs as { path, body, criteria: [criterionNumbers] }. Do not skip a criterion silently.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_specs exactly once with { specs }. Stop.`,\n  tools: ['submit_specs'],\n}\n\nexport function createQaAuthorAgent(config: QaAuthorConfig) {\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_specs', description: 'Submit the spec stubs. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(prd: string): Promise<QaResult> {\n    if (!prd?.trim()) throw new Error('qa author requires a PRD')\n    emit('specs', 'start')\n    const { specs } = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `PRD:\\n${fenceUntrustedContent(prd)}`,\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    // Flag criteria with no spec (only when the caller told us how many to expect).\n    const covered = new Set(specs.flatMap((s) => s.criteria))\n    const uncovered = config.criteriaCount\n      ? Array.from({ length: config.criteriaCount }, (_, i) => i + 1).filter((n) => !covered.has(n))\n      : []\n    emit('specs', 'ok', `${specs.length} spec(s), ${uncovered.length} uncovered`)\n    return { specs, uncovered, requiresReview: true }\n  }\n\n  return {\n    name: 'coding-qa-author',\n    run,\n    asHandle() {\n      return { name: 'coding-qa-author', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# QA Author\n\nTurns a PRD's acceptance criteria into **typed Vitest spec stubs** — one+ block per criterion, with coverage tracking.\n\n```bash\nnpx agentskit add coding-qa-author\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createQaAuthorAgent } from './agents/coding-qa-author/agent'\n\nconst r = await createQaAuthorAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n  criteriaCount: 5, // optional — enables uncovered-criteria tracking\n}).run(prdJson)\n// → { specs: [{ path, body, criteria[] }], uncovered[], requiresReview }\n```\n\n- **Typed specs** — `invokeStructured` + zod; each spec records which criterion number(s) it covers, so the file can be written verbatim.\n- **Coverage tracking** — supply `criteriaCount` and any criterion with no spec is surfaced in `uncovered`, never silently dropped.\n- Untrusted PRD text is **fenced**.\n\n`run(prd)` → `QaResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md). Fed by [`coding-prd-author`](../coding-prd-author); feeds [`coding-dev-implementer`](../coding-dev-implementer).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\nexport const suite: EvalSuite = {\n  name: 'coding-qa-author',\n  cases: [\n    {\n      input: `PRD JSON: {\"criteria\":[\"1. calculateTax(amount, region) returns the correct VAT for 'EU' at 21%\",\"2. calculateTax throws on an unknown region\"]}. The project uses Vitest and names spec files *.test.ts next to the source. Produce the spec stubs.`,\n      expected: (r: string) => /describe\\(/.test(r) && /\\bit\\(/.test(r) && /\\.test\\.ts/i.test(r),\n    },\n    {\n      input: `PRD JSON: {\"criteria\":[\"1. The /health endpoint returns 200 with { status: 'ok' } when the database is reachable\"]}. Produce a Vitest spec stub referencing criterion 1.`,\n      expected: (r: string) => /criterion 1/i.test(r) && /expect\\(/.test(r),\n    },\n    {\n      input: `PRD JSON: {\"criteria\":[\"1. debounce(fn, ms) only invokes fn once after rapid successive calls within the window\"]}. Generate the spec stub; you may use expect.assertions and todo placeholders but no it.skip.`,\n      expected: (r: string) => /describe\\(/.test(r) && !/it\\.skip/.test(r),\n    },\n    {\n      input: `PRD JSON: {\"criteria\":[]}. The acceptance criteria list is empty. Produce the Vitest spec stubs.`,\n      expected: (r: string) => /(no (criteria|criterion)|empty|nothing to|cannot|missing|escalat|need)/i.test(r),\n    },\n  ],\n}\n"}],"installable":true}