{"id":"coding-issue-creator","title":"Issue Creator","description":"Drafts one TYPED GitHub issue per PRD acceptance criterion, then creates them through a caller-injected createIssue transport. Real creation or honest draft (no transport => drafts + created:[], never a faked number); fail-closed HITL (no approve + autoApprove off => nothing created); reports the real issue number/url.","category":"coding","version":"2.0.0","source":"AKOS","license":"MIT","tags":["coding","human-in-the-loop","github"],"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-issue-creator","description":"Drafts one TYPED GitHub issue per PRD acceptance criterion, then creates them through a caller-injected createIssue transport. Real creation or honest draft (no transport => drafts + created:[], never a faked number); fail-closed HITL (no approve + autoApprove off => nothing created); reports the real issue number/url.","systemPrompt":"You translate a PRD into GitHub issues — ONE issue per acceptance criterion (never batch\nmultiple criteria into one issue). For each criterion: title = the first 80 characters; body = the\ncriterion in full plus a Definition of Done checklist; labels = [\"enhancement\",\"automated\"] unless the\ncriterion implies a bug fix, then [\"bug\",\"automated\"].\n\nYou do NOT create anything — creation happens outside you. Just return the drafted issues.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_issues exactly once with { issues: [{ title, body, labels }] }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.coding-issue-creator","name":"Issue Creator","description":"Drafts one TYPED GitHub issue per PRD acceptance criterion, then creates them through a caller-injected createIssue transport. Real creation or honest draft (no transport => drafts + created:[], never a faked number); fail-closed HITL (no approve + autoApprove off => nothing created); reports the real issue number/url.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"coding-issue-creator","description":"Drafts one TYPED GitHub issue per PRD acceptance criterion, then creates them through a caller-injected createIssue transport. Real creation or honest draft (no transport => drafts + created:[], never a faked number); fail-closed HITL (no approve + autoApprove off => nothing created); reports the real issue number/url.","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 * Issue Creator — drafts one TYPED GitHub issue per PRD acceptance criterion, then creates\n * them through a caller-injected `createIssue` transport. The model only drafts; creation\n * is deterministic code that reports the real issue number/url — it never claims an issue\n * it didn't open.\n *\n *   - **One issue per criterion** — the invariant, enforced in the draft step.\n *   - **Real creation or honest draft** — no transport ⇒ you get the drafted issues and\n *     `created:[]`, never a faked issue number.\n *   - **Fail-closed HITL** — each creation is gated by `approve`; with no `approve` and\n *     `autoApprove` off (the default) nothing is created.\n *\n * ```ts\n * const r = await createIssueCreatorAgent({\n *   adapter,\n *   createIssue: (i) => gh.issues.create({ ...i }).then((res) => ({ number: res.number, url: res.html_url })),\n *   approve: (i) => ui.confirm(`Open issue \"${i.title}\"?`),\n * }).run(prdJson)\n * ```\n */\n\nexport interface IssueDraft {\n  title: string\n  body: string\n  labels: string[]\n}\n\nexport interface CreatedIssue {\n  title: string\n  ok: boolean\n  number?: number\n  url?: string\n  error?: string\n  skipped?: boolean\n}\n\nexport interface IssueCreatorResult {\n  drafts: IssueDraft[]\n  created: CreatedIssue[]\n  /** True when ≥1 issue was held back pending approval. */\n  requiresApproval: boolean\n}\n\nexport interface IssueCreatorConfig {\n  adapter: AdapterFactory\n  /** Transport that actually opens an issue and returns its number + url. */\n  createIssue?: (issue: IssueDraft) => Promise<{ number: number; url: string }> | { number: number; url: string }\n  /** HITL gate per issue before creating. Return false to hold back. */\n  approve?: (issue: IssueDraft) => boolean | Promise<boolean>\n  /** Create without an `approve` gate. Default false (fail-closed). */\n  autoApprove?: boolean\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Output = z.object({\n  issues: z.array(z.object({\n    title: z.string().max(80),\n    body: z.string(),\n    labels: z.array(z.string()),\n  })),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'issue-creator',\n  description: 'Drafts one typed GitHub issue per PRD acceptance criterion (creation is gated, deterministic code).',\n  systemPrompt: `You translate a PRD into GitHub issues — ONE issue per acceptance criterion (never batch\nmultiple criteria into one issue). For each criterion: title = the first 80 characters; body = the\ncriterion in full plus a Definition of Done checklist; labels = [\"enhancement\",\"automated\"] unless the\ncriterion implies a bug fix, then [\"bug\",\"automated\"].\n\nYou do NOT create anything — creation happens outside you. Just return the drafted issues.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_issues exactly once with { issues: [{ title, body, labels }] }. Stop.`,\n  tools: ['submit_issues'],\n}\n\nexport function createIssueCreatorAgent(config: IssueCreatorConfig) {\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_issues', description: 'Submit the drafted issues. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(prd: string): Promise<IssueCreatorResult> {\n    if (!prd?.trim()) throw new Error('issue creator requires a PRD')\n    emit('draft', 'start')\n    const { issues: drafts } = 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    emit('draft', 'ok', `${drafts.length} issue(s)`)\n\n    const created: CreatedIssue[] = []\n    let requiresApproval = false\n    if (!config.createIssue) {\n      // No transport — return drafts honestly, create nothing.\n      return { drafts, created: [], requiresApproval: false }\n    }\n    for (const issue of drafts) {\n      const approved = config.approve ? await config.approve(issue) : config.autoApprove === true\n      if (!approved) {\n        requiresApproval = true\n        created.push({ title: issue.title, ok: false, skipped: true, error: 'not approved' })\n        emit('create', 'skip', issue.title)\n        continue\n      }\n      try {\n        emit('create', 'start', issue.title)\n        const { number, url } = await config.createIssue(issue)\n        created.push({ title: issue.title, ok: true, number, url })\n        emit('create', 'ok', `#${number}`)\n      } catch (err) {\n        created.push({ title: issue.title, ok: false, error: err instanceof Error ? err.message : String(err) })\n        emit('create', 'error', issue.title)\n      }\n    }\n    return { drafts, created, requiresApproval }\n  }\n\n  return {\n    name: 'coding-issue-creator',\n    run,\n    asHandle() {\n      return { name: 'coding-issue-creator', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Issue Creator\n\nDrafts **one typed GitHub issue per PRD acceptance criterion**, then creates them through your transport — real creation or an honest draft.\n\n```bash\nnpx agentskit add coding-issue-creator\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createIssueCreatorAgent } from './agents/coding-issue-creator/agent'\n\nconst r = await createIssueCreatorAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n  createIssue: (i) => gh.issues.create(i).then((res) => ({ number: res.number, url: res.html_url })),\n  approve: (i) => ui.confirm(`Open issue \"${i.title}\"?`),\n}).run(prdJson)\n// → { drafts: [{ title, body, labels }], created: [{ title, ok, number?, url?, error? }], requiresApproval }\n```\n\n> The previous version told the model to call `github.createIssue` but wired **no tools** — it could only *claim* to create issues.\n\n- **One issue per criterion** — the invariant, enforced in the typed draft (`invokeStructured` + zod).\n- **Real creation or honest draft** — code calls your `createIssue` and records the **actual** number/url; no transport ⇒ drafts + `created:[]`, never a faked issue.\n- **Fail-closed HITL** — each creation gated by `approve`; no `approve` + `autoApprove` off ⇒ nothing created. Untrusted PRD text is **fenced**.\n\n`run(prd)` → `IssueCreatorResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md). Fed by [`coding-prd-author`](../coding-prd-author).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\nexport const suite: EvalSuite = {\n  name: 'coding-issue-creator',\n  cases: [\n    {\n      input: `PRD JSON: {\"problem\":\"Users cannot reset passwords\",\"criteria\":[\"Add a POST /auth/reset endpoint that emails a one-time token\",\"Token expires after 15 minutes and is single-use\",\"Rate-limit reset requests to 5 per hour per email\"]}. Create the GitHub issues.`,\n      expected: (r: string) => /enhancement/i.test(r) && /(definition of done|\\bDoD\\b|checklist|- \\[ \\])/i.test(r),\n    },\n    {\n      input: `PRD JSON: {\"problem\":\"Search returns stale results\",\"criteria\":[\"Fix the cache invalidation bug so updated documents appear within 2 seconds\"]}. Create the GitHub issue.`,\n      expected: (r: string) => /bug/i.test(r) && /automated/i.test(r),\n    },\n    {\n      input: `PRD JSON: {\"criteria\":[\"Implement OAuth2 PKCE login flow for the mobile client including token refresh, secure storage of the refresh token, and graceful handling of revoked tokens across app restarts and network failures\"]}. Create the issue; the criterion is over 80 characters so set the title accordingly.`,\n      expected: (r: string) => /title/i.test(r) && /(80|truncat)/i.test(r),\n    },\n    {\n      input: `PRD JSON: {\"problem\":\"Dashboard is slow\",\"criteria\":[]}. The acceptance criteria array is empty. Create the issues.`,\n      expected: (r: string) => /(no (criteria|criterion)|empty|cannot|nothing to|missing|need|escalat)/i.test(r),\n    },\n  ],\n}\n"}],"installable":true}