// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 avtc /** Register the add_to_backlog tool — adds a new feature to the kanban board backlog. */ import { basename } from "node:path"; import { Type } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import type { KanbanContext } from "../kanban/kanban-context.js"; import { textResult } from "./text-result.js"; /** Register the add_to_backlog tool. */ export function registerAddToBacklogTool(pi: ExtensionAPI, ctx: KanbanContext): void { const { getDatabase } = ctx; pi.registerTool({ name: "add_to_backlog", label: "Add Feature to Backlog", description: [ "Add a new feature to the kanban board backlog.", "Use when decomposing a feature into smaller parts (after user confirmation),", "or when the user requests adding a new feature to the board.", "Returns the feature ID and slug for reference.", ].join(" "), parameters: Type.Object({ slug: Type.String({ description: "Unique slug for the feature (YYYY-MM-DD- format). Must match the design-doc/plan-doc filename pattern.", }), title: Type.String({ maxLength: 200, description: "Short, descriptive title for the feature", }), description: Type.String({ description: [ "Detailed feature description with all known context, requirements, and constraints.", "Include everything a future agent session would need to start working on this feature", "without any prior context — problem statement, proposed approach, key decisions,", "relevant code areas, and any user preferences discussed.", ].join(" "), }), }), async execute(_toolCallId, params, _signal, _onUpdate, _toolCtx) { const database = await getDatabase(); // Resolve or auto-create project const { detectProject } = await import("../kanban/data/kanban-detect-project.js"); let projectId = await detectProject(database, process.cwd()); if (!projectId) { const cwd = process.cwd(); // Check if a project with this repo path already exists (e.g. created by a previous call) const existingProject = database.findProjectByRepoPath(cwd); if (existingProject) { projectId = existingProject.id; } else { projectId = database.createProject({ name: basename(cwd), repoPath: cwd, }); } } const slug = params.slug; // Check for slug collision const existingFeature = database.findFeatureBySlug(slug, projectId); if (existingFeature) { return textResult( `A feature with slug "${slug}" already exists (ID: ${existingFeature.id}, lane: ${existingFeature.lane}). Not creating duplicate.`, ); } // Prefix description with LLM-generated note const prefixedDescription = "Important: content generated by LLM, based on user description — " + "research and reask user regarding every section and nuances\n\n" + params.description; // Create feature in backlog const featureId = database.createFeature({ projectId, slug, title: params.title, description: prefixedDescription, lane: "backlog", }); return textResult( [ "Feature added to backlog:", `- ID: ${featureId}`, `- Title: ${params.title}`, `- Slug: ${slug}`, "- Lane: backlog", "", "Move it to the design lane to have the auto-designer pick it up, or start a design session manually.", ].join("\n"), ); }, renderCall(args, theme) { let text = theme.fg("toolTitle", theme.bold("add_to_backlog ")); text += theme.fg("accent", `"${args.slug}"`); return new Text(text, 0, 0); }, renderResult(result, _options, theme) { if (result.content?.[0]?.type === "text") { const text = result.content[0].text; if (text.includes("already exists")) { return new Text(theme.fg("error", text), 0, 0); } return new Text(theme.fg("success", text), 0, 0); } return new Text("", 0, 0); }, }); }