import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import type { ChecklistItem, CreateTicketParams, Ticket, TicketPriority, } from "../types.js"; import { nextTicketId, writeTicket, ticketFileName, ensureDir, isInitialized, syncBacklog, contentText, } from "../utils.js"; export function registerDocgraphTicketCreate(pi: ExtensionAPI): void { pi.registerTool({ name: "docgraph_ticket_create", label: "Docgraph Ticket Create", description: "Create a new implementation ticket under docs/tickets/. Each ticket is one unit of work with acceptance criteria and definition of done. Slice tickets VERTICALLY by feature or user-facing capability, NOT horizontally by technical layer: each ticket must be a complete, independently testable and preferably shippable slice that includes all necessary layers (UI, API, business logic, database) required to deliver that capability. Never create separate layer-only tickets (e.g. 'database schema' vs 'API' vs 'frontend') for the same feature; split a feature into multiple tickets only when each slice is itself an independently testable capability or user outcome. Every ticket's Definition of Done includes the mandatory TDD cycle: test written first (Red), minimal implementation (Green), refactor, and a passing relevant test suite.", parameters: Type.Object({ title: Type.String({ description: "Short, descriptive ticket title" }), priority: StringEnum(["P0", "P1", "P2", "P3"] as const), estimate: Type.Optional( Type.String({ description: "Estimated effort (e.g. '2h', '1d', 'S', 'M', 'L')", }), ), dependencies: Type.Optional( Type.Array(Type.String(), { description: "Ticket IDs this depends on", }), ), context: Type.Optional( Type.String({ description: "Background information and implementation notes", }), ), acceptanceCriteria: Type.Optional( Type.Array(Type.String(), { description: "Verifiable criteria for acceptance", }), ), definitionOfDone: Type.Optional( Type.Array(Type.String(), { description: "Additional Definition of Done items. The mandatory TDD checklist is always included by default.", }), ), relatedDocs: Type.Optional( Type.Array(Type.String(), { description: "Documentation files relevant to this ticket, as repo-root-relative paths (e.g. 'docs/API.md'); hrefs are rendered relative to the ticket file", }), ), relatedFiles: Type.Optional( Type.Array(Type.String(), { description: "Source files relevant to this ticket", }), ), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const p = params as CreateTicketParams; if (!isInitialized(ctx)) { return { content: [ { type: "text", text: "Documentation not initialized. Run `docgraph_init` first.", }, ], details: { action: "ticket_create", error: "not_initialized", state: { initialized: false, schemaVersion: 1 }, }, }; } ensureDir("docs/tickets", ctx.cwd); const id = nextTicketId(ctx.cwd); const now = new Date().toISOString(); // Checklist inputs arrive as plain text; items start unchecked with no // verification comment. Checked state and inline comments are added later // during implementation and preserved by the ticket round-trip. const toChecklist = (items: string[] = []): ChecklistItem[] => items.map((text) => ({ text, checked: false, comment: "" })); const ticket: Ticket = { id, title: p.title, status: "backlog", priority: p.priority as TicketPriority, estimate: p.estimate, dependencies: p.dependencies ?? [], context: p.context ?? "", acceptanceCriteria: toChecklist(p.acceptanceCriteria), definitionOfDone: [ { text: "Test written first — automated test encoded the expected behavior and acceptance criteria before any production code", checked: false, comment: "", }, { text: "Test confirmed failing for the expected reason (Red)", checked: false, comment: "", }, { text: "Minimum code written to make the test pass (Green)", checked: false, comment: "", }, { text: "Implementation refactored with all tests still passing (Refactor)", checked: false, comment: "", }, { text: "Relevant test suite run — no regressions", checked: false, comment: "", }, ...toChecklist(p.definitionOfDone), ], implementationNotes: [], relatedDocs: p.relatedDocs ?? [], relatedFiles: p.relatedFiles ?? [], createdAt: now, updatedAt: now, }; const ok = writeTicket(ticket, ctx.cwd); // Keep docs/BACKLOG.md in sync with the ticket files. const backlogSynced = ok ? syncBacklog(ctx.cwd) : false; return { content: [ { type: "text", text: ok ? `Created ticket T-${id}: **${p.title}** (${p.priority}) — docs/tickets/${ticketFileName({ id, title: p.title })}` + (backlogSynced ? "" : " (warning: docs/BACKLOG.md not updated)") : `Failed to create ticket T-${id}`, }, ], details: { action: "ticket_create", ticket, success: ok, backlogSynced, state: { initialized: true, schemaVersion: 1 }, }, }; }, renderCall(args, theme) { return new Text( theme.fg("toolTitle", theme.bold("docgraph-ticket-create ")) + theme.fg("muted", args.title), 0, 0, ); }, renderResult(result, _opts, theme) { const text = contentText(result.content?.[0]); const isError = text.startsWith("Failed"); return new Text( isError ? theme.fg("error", text) : theme.fg("success", text), 0, 0, ); }, }); }