/** * Seeds Extension — per-project issue management via the Seeds CLI (`sd`) * * Core issue tools: * seeds_prime — restore workflow context (sd prime) * seeds_list — list / ready / blocked / stats * seeds_show — show one issue in detail * seeds_doctor — project health checks * seeds_create — create a new issue * seeds_update — update an existing issue * seeds_close — close one or more issues * seeds_relation — manage dependencies and blockers * seeds_project — project-level actions (init, sync, onboard) * seeds_plan_prompt — generate a structured planning prompt * seeds_plan_show — inspect a submitted plan and child seeds * seeds_plan_validate — re-validate stored plan structure * seeds_plan_submit — submit a filled plan JSON document * seeds_plan_review — record reviewer metadata on a plan * seeds_plan_outcome — record plan outcome and optional Mulch decision * * Human command surface: * /seeds status — quick project stats * /seeds ready — issues with no blockers * /seeds blocked — blocked issues * /seeds show — issue detail * /seeds prime — workflow context * /seeds doctor — health checks * /seeds-plan ... — inspect plan prompt/show/validate output * /seeds-status — package status/debug */ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { maybeSpillLongText, runSeedsCommand, type SeedsCommandResult, } from "./seeds-cli.js"; // --------------------------------------------------------------------------- // Package metadata // --------------------------------------------------------------------------- const sourcePath = fileURLToPath(import.meta.url); const packageRoot = path.resolve(path.dirname(sourcePath), ".."); interface PackageMetadata { name: string; version: string; packageRoot: string; sourcePath: string; } let cachedPackageMetadata: PackageMetadata | null = null; function getPackageMetadata(): PackageMetadata { if (cachedPackageMetadata) return cachedPackageMetadata; let name = "@davehardy20/pi-seeds"; let version = "0.1.0"; try { const packageJson = JSON.parse( fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"), ) as { name?: string; version?: string }; name = packageJson.name ?? name; version = packageJson.version ?? version; } catch { // best-effort metadata only } cachedPackageMetadata = { name, version, packageRoot, sourcePath }; return cachedPackageMetadata; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Run a seeds command and return a structured tool result */ async function seedsTool( pi: ExtensionAPI, args: string[], cwd: string, signal?: AbortSignal, ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object; }> { const result: SeedsCommandResult = await runSeedsCommand(pi, args, { cwd, signal, }); const text = await maybeSpillLongText(result.text); return { content: [{ type: "text", text }], details: result.details, }; } function toolErrorResult(text: string, details: object = {}) { return { content: [{ type: "text" as const, text }], details, }; } async function seedsPlanSubmitTool( pi: ExtensionAPI, cwd: string, id: string, planText: string, options: { overwrite?: boolean; recordDecision?: boolean }, signal?: AbortSignal, ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object; }> { if (options.recordDecision) { return toolErrorResult( "Mulch decision recording during plan submission is no longer allowed. Record durable decisions only after evaluator-confirmed successful outcomes with `seeds_plan_outcome`.", { kind: "usage_error" }, ); } if (!planText.trim()) { return toolErrorResult( "Plan content is required. Pass the completed JSON document returned from `seeds_plan_prompt`.", { kind: "usage_error" }, ); } let normalizedPlan: string; try { normalizedPlan = `${JSON.stringify(JSON.parse(planText), null, 2)}\n`; } catch { return toolErrorResult( "Plan content must be valid JSON. Pass the completed JSON document returned from `seeds_plan_prompt`.", { kind: "usage_error" }, ); } const planPath = path.join( os.tmpdir(), `pi-seeds-plan-${process.pid}-${Date.now()}.json`, ); await fs.promises.writeFile(planPath, normalizedPlan, "utf8"); try { const args = ["plan", "submit", id, "--plan", planPath]; if (options.overwrite) args.push("--overwrite"); return await seedsTool(pi, args, cwd, signal); } finally { await fs.promises.rm(planPath, { force: true }).catch(() => undefined); } } async function recordMulchDecision( pi: ExtensionAPI, cwd: string, domain: string, description: string, signal?: AbortSignal, ): Promise<{ ok: boolean; text: string }> { const result = await pi.exec( "ml", ["record", domain, "--type", "decision", "--description", description], { cwd, signal, }, ); const output = [result.stdout.trim(), result.stderr.trim()] .filter(Boolean) .join("\n"); if (result.code === 0) { return { ok: true, text: output || `Recorded Mulch decision in domain "${domain}".`, }; } return { ok: false, text: output || `Mulch decision recording failed for domain "${domain}" (exit ${result.code}).`, }; } async function seedsPlanOutcomeTool( pi: ExtensionAPI, cwd: string, params: { id: string; result: "success" | "partial" | "failure"; note?: string; recordDecision?: boolean; decisionDomain?: string; decisionDescription?: string; }, signal?: AbortSignal, ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object; }> { if (params.recordDecision && params.result !== "success") { return toolErrorResult( "Mulch decisions should only be recorded for successful outcomes.", { kind: "usage_error" }, ); } if (params.recordDecision && !params.decisionDomain) { return toolErrorResult( "decisionDomain is required when recordDecision=true.", { kind: "usage_error" }, ); } const args = ["plan", "outcome", params.id, "--result", params.result]; if (params.note) args.push("--note", params.note); const result = await runSeedsCommand(pi, args, { cwd, signal }); let text = await maybeSpillLongText(result.text); let mulchDetails: | { ok: boolean; text: string; domain?: string } | { ok: false; deferred: true; reason: "open_children"; text: string; domain?: string; openChildren: number; } | undefined; if (result.ok && params.recordDecision && params.decisionDomain) { const payload = result.details.payload as | { open_children?: unknown } | undefined; const openChildren = typeof payload?.open_children === "number" ? payload.open_children : null; if (openChildren !== 0) { const deferredText = openChildren === null ? `Deferred Mulch decision recording for ${params.id}: open child count is unknown.` : `Deferred Mulch decision recording for ${params.id}: ${openChildren} child issue${openChildren === 1 ? " is" : "s are"} still open.`; mulchDetails = { ok: false, deferred: true, reason: "open_children", text: deferredText, domain: params.decisionDomain, openChildren: openChildren ?? -1, }; text = `${text}\n\nMulch: ${deferredText}`; } else { const description = params.decisionDescription?.trim() || params.note?.trim() || `Successful Seeds plan outcome recorded for ${params.id}.`; const mulchResult = await recordMulchDecision( pi, cwd, params.decisionDomain, description, signal, ); mulchDetails = { ...mulchResult, domain: params.decisionDomain, }; text = `${text}\n\nMulch: ${mulchResult.text}`; } } return { content: [{ type: "text", text }], details: { ...result.details, mulch: mulchDetails, }, }; } // --------------------------------------------------------------------------- // Extension // --------------------------------------------------------------------------- export default function seedsExtension(pi: ExtensionAPI) { // ----------------------------------------------------------------------- // Status/debug command // ----------------------------------------------------------------------- pi.registerCommand("seeds-status", { description: "Show pi-seeds package status and version", handler: async (_args, ctx) => { const metadata = getPackageMetadata(); const lines = [ `${metadata.name} v${metadata.version}`, `source: ${metadata.sourcePath}`, `packageRoot: ${metadata.packageRoot}`, ]; try { const result = await runSeedsCommand(pi, ["stats"], { cwd: ctx.cwd, }); if (result.ok) { lines.push(`seedsCli: available (${result.details.runner})`); } else if (result.details.binaryMissing) { lines.push("seedsCli: not found (install sd or ensure bun/npm is available)"); } else { lines.push(`seedsCli: available (${result.details.runner})`); } } catch { lines.push("seedsCli: check failed"); } ctx.ui.notify(lines.join("\n"), "info"); }, }); // ----------------------------------------------------------------------- // Tool: seeds_prime // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_prime", label: "Seeds Prime", description: "Fetch Seeds workflow context (sd prime) for the current project. " + "Returns AI-agent-ready context including open issues, workflow rules, and command reference.", promptSnippet: "Fetch Seeds workflow context (sd prime) to restore project issue state after compaction or when entering a new session.", promptGuidelines: [ "Use seeds_prime when you need to understand the current project's issue workflow — especially after compaction or when resuming work in a project with .seeds/.", "Prefer compact=true when you only need a quick reference and want to minimize token usage.", "Do not run seeds_prime on every turn. Call it once when entering a project or recovering context, then use seeds_list or seeds_show for targeted updates.", "If seeds_prime reports 'not a seeds project', suggest running `sd init` to set up .seeds/ before proceeding.", ], parameters: Type.Object({ compact: Type.Optional( Type.Boolean({ description: "Return condensed quick-reference instead of full workflow guide", default: false, }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const args = ["prime"]; if (params.compact) args.push("--compact"); return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_list // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_list", label: "Seeds List", description: "List Seeds issues in the current project. Supports multiple modes: " + "list (with filters), ready (unblocked issues), blocked, and stats.", promptSnippet: "List Seeds issues to find ready work, blocked items, or project statistics.", promptGuidelines: [ "Use mode='ready' to find unblocked issues available for work — this is the most common starting point.", "Use mode='blocked' to identify issues that cannot proceed due to unresolved dependencies.", "Use mode='stats' for a quick project overview (open/closed/blocked counts by type and priority).", "Use mode='list' with filters (status, type, label, assignee) for targeted queries.", "If seeds_list reports 'not a seeds project', suggest running `sd init` to initialize before proceeding.", "Issue IDs are opaque strings like 'project-1lg', not numeric indices.", ], parameters: Type.Object({ mode: StringEnum(["list", "ready", "blocked", "stats"] as const, { description: "list = filtered list, ready = unblocked issues, blocked = blocked issues, stats = project statistics", default: "list", }), status: Type.Optional( StringEnum(["open", "in_progress", "closed"] as const, { description: "Filter by status (list mode only)", }), ), type: Type.Optional( StringEnum(["task", "bug", "feature", "epic"] as const, { description: "Filter by type (list mode only)", }), ), assignee: Type.Optional( Type.String({ description: "Filter by assignee (list mode only)", }), ), label: Type.Optional( Type.String({ description: "Filter: must have ALL labels, comma-separated (list mode only)", }), ), labelAny: Type.Optional( Type.String({ description: "Filter: must have ANY label, comma-separated (list mode only)", }), ), unlabeled: Type.Optional( Type.Boolean({ description: "Filter to issues with no labels (list mode only)", }), ), limit: Type.Optional( Type.Number({ description: "Max issues to return (default 50)", default: 50, }), ), all: Type.Optional( Type.Boolean({ description: "Include closed issues (list mode only)", }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const mode = params.mode ?? "list"; const args: string[] = [mode]; // Add list-mode filters if (mode === "list") { if (params.status) args.push("--status", params.status); if (params.type) args.push("--type", params.type); if (params.assignee) args.push("--assignee", params.assignee); if (params.label) args.push("--label", params.label); if (params.labelAny) args.push("--label-any", params.labelAny); if (params.unlabeled) args.push("--unlabeled"); if (params.all) args.push("--all"); } if (params.limit && mode === "list") { args.push("--limit", String(params.limit)); } return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_show // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_show", label: "Seeds Show", description: "Show detailed information about a single Seeds issue by ID.", promptSnippet: "Show the full details of a specific Seeds issue including description, status, type, priority, and labels.", promptGuidelines: [ "Use seeds_show when you need the full description and metadata for a specific issue — seeds_list only returns summaries.", "Issue IDs are opaque strings like 'project-1lg'. Get IDs from seeds_list or seeds_prime output.", "If the issue is not found, suggest checking the ID or running seeds_list to find valid IDs.", ], parameters: Type.Object({ id: Type.String({ description: "Issue ID (e.g. 'project-1lg')", }), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { return seedsTool(pi, ["show", params.id], ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_doctor // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_doctor", label: "Seeds Doctor", description: "Run Seeds project health checks. Returns structured results for " + "config validity, JSONL integrity, schema validation, duplicate IDs, " + "referential integrity, circular dependencies, and more.", promptSnippet: "Run Seeds project health checks to diagnose data integrity issues or configuration problems.", promptGuidelines: [ "Use seeds_doctor when something seems wrong with the .seeds/ data or when setting up a project for the first time.", "The verbose option shows all checks including passing ones; without it only warnings and failures are notable.", "The fix option auto-fixes fixable issues (like gitattributes). Use it only when the user explicitly requests fixes.", "If doctor reports 'not a seeds project', suggest running `sd init` to initialize.", ], parameters: Type.Object({ verbose: Type.Optional( Type.Boolean({ description: "Show all check results including passes", default: false, }), ), fix: Type.Optional( Type.Boolean({ description: "Auto-fix fixable issues (use only on explicit user request)", default: false, }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const args = ["doctor"]; if (params.verbose) args.push("--verbose"); if (params.fix) args.push("--fix"); return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_create // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_create", label: "Seeds Create", description: "Create a new Seeds issue in the current project.", promptSnippet: "Create a Seeds issue to track work, bugs, features, or epics.", promptGuidelines: [ "Use seeds_create to file new work before starting implementation.", "Set an appropriate type (task, bug, feature, epic) and priority (0=critical, 4=backlog).", "Add labels when they help organize the issue (e.g. 'backend', 'ui', 'docs').", "If seeds_create reports 'not a seeds project', suggest running `sd init` first.", ], parameters: Type.Object({ title: Type.String({ description: "Issue title" }), type: Type.Optional( StringEnum(["task", "bug", "feature", "epic"] as const, { description: "Issue type", default: "task", }), ), priority: Type.Optional( Type.Number({ description: "Priority 0-4 (0=critical, 2=medium, 4=backlog)", default: 2, }), ), description: Type.Optional( Type.String({ description: "Issue description" }), ), assignee: Type.Optional(Type.String({ description: "Assignee name" })), labels: Type.Optional( Type.String({ description: "Comma-separated labels", }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const args = ["create", "--title", params.title]; if (params.type) args.push("--type", params.type); if (params.priority !== undefined) { args.push("--priority", String(params.priority)); } if (params.description) args.push("--description", params.description); if (params.assignee) args.push("--assignee", params.assignee); if (params.labels) args.push("--labels", params.labels); return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_update // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_update", label: "Seeds Update", description: "Update fields on an existing Seeds issue.", promptSnippet: "Update a Seeds issue's status, title, priority, assignee, description, type, or labels.", promptGuidelines: [ "Use seeds_update to claim work (status=in_progress), reassign, reprioritize, or correct issue details.", "Only include the fields you want to change; omitted fields are left untouched.", "Label changes use addLabels/removeLabels/setLabels to avoid ambiguity.", "If the issue is not found, suggest checking the ID or running seeds_list.", ], parameters: Type.Object({ id: Type.String({ description: "Issue ID" }), status: Type.Optional( StringEnum(["open", "in_progress", "closed"] as const, { description: "New status", }), ), title: Type.Optional(Type.String({ description: "New title" })), type: Type.Optional( StringEnum(["task", "bug", "feature", "epic"] as const, { description: "New type", }), ), priority: Type.Optional( Type.Number({ description: "New priority 0-4", }), ), assignee: Type.Optional(Type.String({ description: "New assignee" })), description: Type.Optional( Type.String({ description: "New description" }), ), addLabels: Type.Optional( Type.String({ description: "Comma-separated labels to add", }), ), removeLabels: Type.Optional( Type.String({ description: "Comma-separated labels to remove", }), ), setLabels: Type.Optional( Type.String({ description: "Comma-separated labels to set (replaces all)", }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const args = ["update", params.id]; if (params.status) args.push("--status", params.status); if (params.title) args.push("--title", params.title); if (params.type) args.push("--type", params.type); if (params.priority !== undefined) { args.push("--priority", String(params.priority)); } if (params.assignee) args.push("--assignee", params.assignee); if (params.description) args.push("--description", params.description); if (params.addLabels) args.push("--add-label", params.addLabels); if (params.removeLabels) args.push("--remove-label", params.removeLabels); if (params.setLabels) args.push("--set-labels", params.setLabels); return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_close // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_close", label: "Seeds Close", description: "Close one or more Seeds issues.", promptSnippet: "Close completed Seeds issues. Optionally provide a reason.", promptGuidelines: [ "Use seeds_close when work is complete and the issue should no longer appear in ready/list queries.", "You can close multiple issues at once by passing a comma-separated list of IDs.", "Only close issues after verifying the work is actually done.", ], parameters: Type.Object({ ids: Type.String({ description: "Comma-separated issue IDs to close (e.g. 'project-a1b2,project-c3d4')", }), reason: Type.Optional(Type.String({ description: "Close reason" })), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const ids = params.ids.split(",").map((s) => s.trim()); const args = ["close", ...ids]; if (params.reason) args.push("--reason", params.reason); return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_relation // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_relation", label: "Seeds Relation", description: "Manage issue dependencies and blockers. Actions: add_dep, remove_dep, list_dep, block, unblock.", promptSnippet: "Add or remove dependencies between issues, or mark issues as blocked/unblocked.", promptGuidelines: [ "Use add_dep when one issue cannot start until another is finished.", "Use block to mark an issue as blocked by another issue.", "Use unblock to clear a blocker.", "Use list_dep to see the dependency graph for a specific issue.", "Issue IDs are opaque strings like 'project-1lg'.", ], parameters: Type.Object({ action: Type.String({ description: "Action: add_dep, remove_dep, list_dep, block, unblock", }), id: Type.String({ description: "Source issue ID" }), targetId: Type.Optional( Type.String({ description: "Target issue ID (required for add_dep, remove_dep, block, unblock)", }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { let args: string[]; switch (params.action) { case "add_dep": args = ["dep", "add", params.id, params.targetId ?? ""]; break; case "remove_dep": args = ["dep", "remove", params.id, params.targetId ?? ""]; break; case "list_dep": args = ["dep", "list", params.id]; break; case "block": args = ["block", params.id, "--by", params.targetId ?? ""]; break; case "unblock": args = ["unblock", params.id, "--from", params.targetId ?? ""]; break; default: return { content: [ { type: "text", text: `Unknown action: ${params.action}. Use add_dep, remove_dep, list_dep, block, or unblock.`, }, ], details: {}, }; } return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_project // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_project", label: "Seeds Project", description: "Project-level Seeds actions: init, sync_status, sync_dry_run, sync, onboard_check, onboard.", promptSnippet: "Initialize, sync, or onboard a Seeds project.", promptGuidelines: [ "Use init when a project does not yet have .seeds/ and you want to start tracking issues.", "Use sync_status to check whether .seeds/ changes need to be committed without committing them.", "Use sync_dry_run to preview what sync would do.", "Use sync only when you are ready to stage and commit .seeds/ changes to git.", "Use onboard to add Seeds guidance to CLAUDE.md / AGENTS.md.", ], parameters: Type.Object({ action: Type.String({ description: "Action: init, sync_status, sync_dry_run, sync, onboard_check, onboard", }), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { let args: string[]; switch (params.action) { case "init": args = ["init"]; break; case "sync_status": args = ["sync", "--status"]; break; case "sync_dry_run": args = ["sync", "--dry-run"]; break; case "sync": args = ["sync"]; break; case "onboard_check": args = ["onboard", "--check"]; break; case "onboard": args = ["onboard"]; break; default: return { content: [ { type: "text", text: `Unknown action: ${params.action}. Use init, sync_status, sync_dry_run, sync, onboard_check, or onboard.`, }, ], details: {}, }; } return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_plan_prompt // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_plan_prompt", label: "Seeds Plan Prompt", description: "Generate a structured planning prompt for a Seeds issue using `sd plan prompt`.", promptSnippet: "Use Seeds planning to decompose large or ambiguous work before implementation.", promptGuidelines: [ "Use seeds_plan_prompt for large, ambiguous, or multi-step work that should be decomposed into child Seeds issues before implementation.", "This is best used by a planner or lead agent before any code changes start.", "Let Seeds auto-select the template from the issue type unless you specifically need to force a template such as refactor or a custom template.", "If you already know the relevant Mulch domain, pass domain so the planning prompt can include better prior art.", ], parameters: Type.Object({ id: Type.String({ description: "Seed issue ID to plan (for example: 'project-1lg')", }), template: Type.Optional( Type.String({ description: "Optional template override (for example: feature, bug, refactor, or a custom template)", }), ), domain: Type.Optional( Type.String({ description: "Optional Mulch domain name to enrich prior art for the planning prompt", }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { const args = ["plan", "prompt", params.id]; if (params.template) args.push("--template", params.template); if (params.domain) args.push("--domain", params.domain); return seedsTool(pi, args, ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_plan_submit // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_plan_submit", label: "Seeds Plan Submit", description: "Submit a completed Seeds plan JSON document and spawn child issues safely without passing arbitrary file paths.", promptSnippet: "Submit a completed Seeds plan so child issues and dependency wiring are created automatically.", promptGuidelines: [ "Pass the completed JSON document produced from seeds_plan_prompt as the plan string.", "Use overwrite only when revising an existing submitted plan for the same seed.", "Do not use recordDecision during plan submission; durable Mulch decisions belong in evaluator-confirmed successful outcomes.", "Use this after the planning prompt has been filled in and reviewed, not as a substitute for planning.", ], parameters: Type.Object({ id: Type.String({ description: "Seed issue ID whose plan should be submitted", }), plan: Type.String({ description: "Completed JSON plan document returned from seeds_plan_prompt after you fill in its sections", }), overwrite: Type.Optional( Type.Boolean({ description: "Overwrite an existing submitted plan for the same seed", default: false, }), ), recordDecision: Type.Optional( Type.Boolean({ description: "Deprecated/disabled. Durable Mulch decisions must be recorded only after evaluator-confirmed successful outcomes.", default: false, }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { return seedsPlanSubmitTool( pi, ctx.cwd, params.id, params.plan, { overwrite: params.overwrite, recordDecision: params.recordDecision, }, signal, ); }, }); // ----------------------------------------------------------------------- // Tool: seeds_plan_show // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_plan_show", label: "Seeds Plan Show", description: "Show a submitted Seeds plan, including child issue decomposition and nested plan context.", promptSnippet: "Inspect a submitted Seeds plan and its spawned child work.", promptGuidelines: [ "Use seeds_plan_show to inspect the approved plan behind a seed before executing or reviewing child work.", "You can pass either the parent seed ID or the plan ID if you already have it.", "Use this to recover context when a task references child Seeds spawned from a plan.", ], parameters: Type.Object({ id: Type.String({ description: "Plan ID or parent seed ID", }), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { return seedsTool(pi, ["plan", "show", params.id], ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_plan_validate // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_plan_validate", label: "Seeds Plan Validate", description: "Re-validate an existing Seeds plan against its template and structure rules.", promptSnippet: "Validate a stored Seeds plan before relying on it or after revising it.", promptGuidelines: [ "Use seeds_plan_validate to confirm a stored plan is still structurally valid before execution or after a revision.", "This is especially useful when a plan was overwritten or when nested planning is involved.", "If validation fails, inspect the plan with seeds_plan_show and regenerate or revise it before continuing.", ], parameters: Type.Object({ id: Type.String({ description: "Plan ID or parent seed ID to validate", }), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { return seedsTool(pi, ["plan", "validate", params.id], ctx.cwd, signal); }, }); // ----------------------------------------------------------------------- // Tool: seeds_plan_review // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_plan_review", label: "Seeds Plan Review", description: "Record reviewer metadata on a Seeds plan without changing execution state.", promptSnippet: "Record that a reviewer has reviewed a stored Seeds plan.", promptGuidelines: [ "Use seeds_plan_review after a reviewer has inspected a plan and you want that review recorded in Seeds.", "Pass the reviewer name with reviewer so the plan metadata shows who reviewed it.", "This records review metadata only; it is not an approval gate by itself.", ], parameters: Type.Object({ id: Type.String({ description: "Plan ID or parent seed ID", }), reviewer: Type.String({ description: "Reviewer name to record with the plan", }), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { return seedsTool( pi, ["plan", "review", params.id, "--by", params.reviewer], ctx.cwd, signal, ); }, }); // ----------------------------------------------------------------------- // Tool: seeds_plan_outcome // ----------------------------------------------------------------------- pi.registerTool({ name: "seeds_plan_outcome", label: "Seeds Plan Outcome", description: "Record the outcome of a Seeds plan and optionally persist a confirmed decision to Mulch.", promptSnippet: "Record plan outcome after implementation and validation are complete.", promptGuidelines: [ "Use seeds_plan_outcome after the relevant child work has been executed and evaluated.", "Set result to success, partial, or failure and add a short note when it helps future recovery.", "Only set recordDecision=true for validated successful outcomes that should become durable Mulch guidance.", "Prefer recordDecision only when the plan is actually closing out and open child count is zero.", "When recording a Mulch decision, pass decisionDomain and optionally decisionDescription to control what gets stored.", ], parameters: Type.Object({ id: Type.String({ description: "Plan ID or parent seed ID", }), result: StringEnum(["success", "partial", "failure"] as const, { description: "Outcome result to record", }), note: Type.Optional( Type.String({ description: "Optional note explaining the outcome", }), ), recordDecision: Type.Optional( Type.Boolean({ description: "Also record a confirmed decision to Mulch after a successful outcome", default: false, }), ), decisionDomain: Type.Optional( Type.String({ description: "Mulch domain name to store the decision under when recordDecision=true", }), ), decisionDescription: Type.Optional( Type.String({ description: "Optional Mulch decision description; falls back to note or a generated summary", }), ), }), async execute(_toolCallId, params, signal, _onUpdate, ctx) { return seedsPlanOutcomeTool(pi, ctx.cwd, params, signal); }, }); // ----------------------------------------------------------------------- // Command: /seeds // ----------------------------------------------------------------------- const SEEDS_SUBCOMMANDS = [ "status", "ready", "blocked", "show", "prime", "doctor", "create", "update", "close", "init", "sync", "onboard", ] as const; const SEEDS_PLAN_SUBCOMMANDS = [ "prompt", "show", "validate", "review", "outcome", ] as const; function formatCommandHelp(): string { return [ "**Usage:** /seeds ", "", "**Subcommands:**", " status — Project statistics", " ready — Issues with no unresolved blockers", " blocked — Issues that are blocked", " show — Show issue detail", " prime — AI workflow context", " doctor — Project health checks", " create — Create a new issue (interactive)", " update — Update an issue (interactive)", " close — Close one or more issues", " init — Initialize .seeds/ in current project", " sync — Stage and commit .seeds/ changes", " onboard — Add Seeds section to project docs", "", "Plan helpers live under `/seeds-plan`.", "Run `/seeds status` to get started.", ].join("\n"); } function formatPlanCommandHelp(): string { return [ "**Usage:** /seeds-plan ", "", "**Subcommands:**", " prompt — Generate a structured planning prompt", " show — Show a submitted plan and child issue tree", " validate — Re-validate a stored plan", " review — Record review metadata on a plan", " outcome [note]— Record success|partial|failure with an optional note", "", "Use the `seeds_plan_submit` tool to submit completed plan JSON safely.", ].join("\n"); } pi.registerCommand("seeds", { description: "Seeds issue tracker commands", getArgumentCompletions: (prefix: string) => { const lowered = prefix.toLowerCase(); const matches = SEEDS_SUBCOMMANDS.filter((s) => s.startsWith(lowered), ).map((s) => ({ value: s, label: s })); return matches.length > 0 ? matches : null; }, handler: async (args, ctx) => { const parts = (args ?? "").trim().split(/\s+/); const sub = parts[0]?.toLowerCase(); if (!sub) { ctx.ui.notify(formatCommandHelp(), "info"); return; } let cmdArgs: string[]; switch (sub) { case "status": cmdArgs = ["stats"]; break; case "ready": cmdArgs = ["ready"]; break; case "blocked": cmdArgs = ["blocked"]; break; case "show": { const id = parts[1]; if (!id) { ctx.ui.notify("Usage: /seeds show ", "warning"); return; } cmdArgs = ["show", id]; break; } case "prime": cmdArgs = ["prime"]; break; case "doctor": cmdArgs = ["doctor"]; break; case "create": cmdArgs = ["create"]; break; case "update": cmdArgs = ["update"]; break; case "close": { const closeIds = parts.slice(1); if (closeIds.length === 0) { ctx.ui.notify("Usage: /seeds close [ids...]", "warning"); return; } cmdArgs = ["close", ...closeIds]; break; } case "init": cmdArgs = ["init"]; break; case "sync": cmdArgs = ["sync"]; break; case "onboard": cmdArgs = ["onboard"]; break; default: ctx.ui.notify( `Unknown subcommand "${sub}".\n\n${formatCommandHelp()}`, "warning", ); return; } const result = await runSeedsCommand(pi, cmdArgs, { cwd: ctx.cwd }); const text = await maybeSpillLongText(result.text); ctx.ui.notify(text, result.ok ? "info" : "error"); }, }); pi.registerCommand("seeds-plan", { description: "Seeds plan workflow commands", getArgumentCompletions: (prefix: string) => { const lowered = prefix.toLowerCase(); const matches = SEEDS_PLAN_SUBCOMMANDS.filter((s) => s.startsWith(lowered), ).map((s) => ({ value: s, label: s })); return matches.length > 0 ? matches : null; }, handler: async (args, ctx) => { const parts = (args ?? "").trim().split(/\s+/); const sub = parts[0]?.toLowerCase(); const id = parts[1]; if (!sub) { ctx.ui.notify(formatPlanCommandHelp(), "info"); return; } if (!id) { ctx.ui.notify(formatPlanCommandHelp(), "warning"); return; } let cmdArgs: string[]; switch (sub) { case "prompt": cmdArgs = ["plan", "prompt", id]; break; case "show": cmdArgs = ["plan", "show", id]; break; case "validate": cmdArgs = ["plan", "validate", id]; break; case "review": { const reviewer = parts[2]; if (!reviewer) { ctx.ui.notify( "Usage: /seeds-plan review ", "warning", ); return; } cmdArgs = ["plan", "review", id, "--by", reviewer]; break; } case "outcome": { const resultValue = parts[2]; const note = parts.slice(3).join(" ").trim(); if (!resultValue) { ctx.ui.notify( "Usage: /seeds-plan outcome [note]", "warning", ); return; } cmdArgs = ["plan", "outcome", id, "--result", resultValue]; if (note) cmdArgs.push("--note", note); break; } default: ctx.ui.notify( `Unknown subcommand "${sub}".\n\n${formatPlanCommandHelp()}`, "warning", ); return; } const result = await runSeedsCommand(pi, cmdArgs, { cwd: ctx.cwd }); const text = await maybeSpillLongText(result.text); ctx.ui.notify(text, result.ok ? "info" : "error"); }, }); }