coding·Registry checks passed · v2.0.0

Issue Creator

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.

codinghuman-in-the-loopgithub

Install

npx agentskit add coding-issue-creator

Quick start

import { openai } from '@agentskit/adapters'import { createCodingIssueCreatorAgent } from './agents/coding-issue-creator/agent'const agent = createCodingIssueCreatorAgent({  adapter: openai({    apiKey: process.env.OPENAI_API_KEY!,    model: 'gpt-4o',  }),})const result = await agent.run('Describe your task here')console.log(result.content)

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createIssueCreatorAgent } from './agents/coding-issue-creator/agent'const r = await createIssueCreatorAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),  createIssue: (i) => gh.issues.create(i).then((res) => ({ number: res.number, url: res.html_url })),  approve: (i) => ui.confirm(`Open issue "${i.title}"?`),}).run(prdJson)// → { drafts: [{ title, body, labels }], created: [{ title, ok, number?, url?, error? }], requiresApproval }

Extend it

Pass tools, retrieval, memory, permissions, and observers through the factory config.

const agent = createCodingIssueCreatorAgent({  adapter,  tools,  retriever,  memory,  onConfirm: (call) => approve(call),  observers: [tracer],})
View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * Issue Creator — drafts one TYPED GitHub issue per PRD acceptance criterion, then creates * them through a caller-injected `createIssue` transport. The model only drafts; creation * is deterministic code that reports the real issue number/url — it never claims an issue * it didn't open. * *   - **One issue per criterion** — the invariant, enforced in the draft step. *   - **Real creation or honest draft** — no transport ⇒ you get the drafted issues and *     `created:[]`, never a faked issue number. *   - **Fail-closed HITL** — each creation is gated by `approve`; with no `approve` and *     `autoApprove` off (the default) nothing is created. * * ```ts * const r = await createIssueCreatorAgent({ *   adapter, *   createIssue: (i) => gh.issues.create({ ...i }).then((res) => ({ number: res.number, url: res.html_url })), *   approve: (i) => ui.confirm(`Open issue "${i.title}"?`), * }).run(prdJson) * ``` */export interface IssueDraft {  title: string  body: string  labels: string[]}export interface CreatedIssue {  title: string  ok: boolean  number?: number  url?: string  error?: string  skipped?: boolean}export interface IssueCreatorResult {  drafts: IssueDraft[]  created: CreatedIssue[]  /** True when ≥1 issue was held back pending approval. */  requiresApproval: boolean}export interface IssueCreatorConfig {  adapter: AdapterFactory  /** Transport that actually opens an issue and returns its number + url. */  createIssue?: (issue: IssueDraft) => Promise<{ number: number; url: string }> | { number: number; url: string }  /** HITL gate per issue before creating. Return false to hold back. */  approve?: (issue: IssueDraft) => boolean | Promise<boolean>  /** Create without an `approve` gate. Default false (fail-closed). */  autoApprove?: boolean  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Output = z.object({  issues: z.array(z.object({    title: z.string().max(80),    body: z.string(),    labels: z.array(z.string()),  })),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = {  name: 'issue-creator',  description: 'Drafts one typed GitHub issue per PRD acceptance criterion (creation is gated, deterministic code).',  systemPrompt: `You translate a PRD into GitHub issues — ONE issue per acceptance criterion (never batchmultiple criteria into one issue). For each criterion: title = the first 80 characters; body = thecriterion in full plus a Definition of Done checklist; labels = ["enhancement","automated"] unless thecriterion implies a bug fix, then ["bug","automated"].You do NOT create anything — creation happens outside you. Just return the drafted issues.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_issues exactly once with { issues: [{ title, body, labels }] }. Stop.`,  tools: ['submit_issues'],}export function createIssueCreatorAgent(config: IssueCreatorConfig) {  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })  }  const submit = (): ToolDefinition =>    defineZodTool({ name: 'submit_issues', description: 'Submit the drafted issues. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition  async function run(prd: string): Promise<IssueCreatorResult> {    if (!prd?.trim()) throw new Error('issue creator requires a PRD')    emit('draft', 'start')    const { issues: drafts } = await invokeStructured({      adapter: config.adapter,      tool: submit(),      task: `PRD:\n${fenceUntrustedContent(prd)}`,      parse: (a) => Output.parse(a),      skill,      memory: config.memory,      observers: config.observers,      onConfirm: config.onConfirm,      maxSteps: config.maxSteps ?? 3,    })    emit('draft', 'ok', `${drafts.length} issue(s)`)    const created: CreatedIssue[] = []    let requiresApproval = false    if (!config.createIssue) {      // No transport — return drafts honestly, create nothing.      return { drafts, created: [], requiresApproval: false }    }    for (const issue of drafts) {      const approved = config.approve ? await config.approve(issue) : config.autoApprove === true      if (!approved) {        requiresApproval = true        created.push({ title: issue.title, ok: false, skipped: true, error: 'not approved' })        emit('create', 'skip', issue.title)        continue      }      try {        emit('create', 'start', issue.title)        const { number, url } = await config.createIssue(issue)        created.push({ title: issue.title, ok: true, number, url })        emit('create', 'ok', `#${number}`)      } catch (err) {        created.push({ title: issue.title, ok: false, error: err instanceof Error ? err.message : String(err) })        emit('create', 'error', issue.title)      }    }    return { drafts, created, requiresApproval }  }  return {    name: 'coding-issue-creator',    run,    asHandle() {      return { name: 'coding-issue-creator', run: async (task: string) => JSON.stringify(await run(task)) }    },  }}
View evaluation contract

Replay these cases with the provider and model you plan to deploy.

import type { EvalSuite } from '@agentskit/eval'export const suite: EvalSuite = {  name: 'coding-issue-creator',  cases: [    {      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.`,      expected: (r: string) => /enhancement/i.test(r) && /(definition of done|\bDoD\b|checklist|- \[ \])/i.test(r),    },    {      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.`,      expected: (r: string) => /bug/i.test(r) && /automated/i.test(r),    },    {      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.`,      expected: (r: string) => /title/i.test(r) && /(80|truncat)/i.test(r),    },    {      input: `PRD JSON: {"problem":"Dashboard is slow","criteria":[]}. The acceptance criteria array is empty. Create the issues.`,      expected: (r: string) => /(no (criteria|criterion)|empty|cannot|nothing to|missing|need|escalat)/i.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category