diff --git a/.plans/workflow-templates.md b/.plans/workflow-templates.md new file mode 100644 index 000000000..a917a0ff8 --- /dev/null +++ b/.plans/workflow-templates.md @@ -0,0 +1,77 @@ +# GSD Workflow Templates — Implementation Plan (Updated) + +**Date:** 2026-03-18 +**Branch:** `feat/workflow-templates` +**Status:** In Progress — Phase 1 + +--- + +## Architecture Mapping (Plan → Actual Codebase) + +The original plan referenced `gsd-tools.cjs`, `lib/init.cjs`, `lib/core.cjs` — these don't exist. +The actual architecture is a TypeScript extension system: + +| Plan Reference | Actual Location | +|---|---| +| `gsd-tools.cjs` command routing | `src/resources/extensions/gsd/commands.ts` | +| `lib/workflow-template.cjs` | `src/resources/extensions/gsd/workflow-templates.ts` (new) | +| `lib/init.cjs` | No separate init; logic lives in handler module | +| `lib/core.cjs` | Utilities spread across `paths.ts`, `state.ts`, etc. | +| `~/.claude/get-shit-done/workflow-templates/` | `src/resources/extensions/gsd/workflow-templates/` (new dir) | +| `/gsd:start`, `/gsd:templates` | `/gsd start`, `/gsd templates` subcommands | +| Prompt templates | `src/resources/extensions/gsd/prompts/` | + +--- + +## Phase 1: Foundation (Core Infrastructure) + +### Files to Create + +1. **`src/resources/extensions/gsd/workflow-templates/registry.json`** + - Template metadata: name, description, phases, triggers, artifact_dir, complexity, agents + +2. **`src/resources/extensions/gsd/workflow-templates.ts`** + - `loadRegistry()` — parse registry.json from extension dir + - `resolveTemplate(nameOrTrigger)` — match by name, alias, or trigger keywords + - `autoDetect(context)` — analyze user input + project state for best template match + - `listTemplates()` — formatted template list for display + - `getTemplateInfo(name)` — detailed template metadata + +3. **`src/resources/extensions/gsd/commands-workflow-templates.ts`** + - `handleStart(args, ctx, pi)` — `/gsd start [template] [args]` + - `handleTemplates(args, ctx)` — `/gsd templates [info ]` + +4. **Wire into `commands.ts`**: + - Add `start` and `templates` to subcommand completions + - Add handler routing for both commands + +### Files to Create (Phase 2 — Templates) + +5. **`src/resources/extensions/gsd/workflow-templates/bugfix.md`** +6. **`src/resources/extensions/gsd/workflow-templates/small-feature.md`** +7. **`src/resources/extensions/gsd/workflow-templates/spike.md`** +8. **`src/resources/extensions/gsd/workflow-templates/hotfix.md`** +9. **`src/resources/extensions/gsd/workflow-templates/refactor.md`** +10. **`src/resources/extensions/gsd/workflow-templates/security-audit.md`** +11. **`src/resources/extensions/gsd/workflow-templates/dep-upgrade.md`** +12. **`src/resources/extensions/gsd/workflow-templates/full-project.md`** + +### Prompt Templates + +13. **`src/resources/extensions/gsd/prompts/workflow-start.md`** — dispatched when `/gsd start` resolves a template +14. **`src/resources/extensions/gsd/prompts/workflow-bugfix.md`** — bugfix-specific dispatch prompt +15. **`src/resources/extensions/gsd/prompts/workflow-small-feature.md`** +16. **`src/resources/extensions/gsd/prompts/workflow-spike.md`** +17. **`src/resources/extensions/gsd/prompts/workflow-hotfix.md`** + +--- + +## Success Criteria + +- [ ] `/gsd start bugfix` resolves template and dispatches workflow prompt +- [ ] `/gsd start` with no args auto-detects from context or shows choices +- [ ] `/gsd templates` lists all available templates +- [ ] `/gsd templates info bugfix` shows detailed template info +- [ ] All existing `/gsd *` commands work unchanged (zero regression) +- [ ] Registry validates (all referenced template files exist) +- [ ] Templates reuse existing agents and prompt patterns diff --git a/src/resources/extensions/gsd/commands-workflow-templates.ts b/src/resources/extensions/gsd/commands-workflow-templates.ts new file mode 100644 index 000000000..02357ea16 --- /dev/null +++ b/src/resources/extensions/gsd/commands-workflow-templates.ts @@ -0,0 +1,544 @@ +/** + * GSD Workflow Template Commands — /gsd start, /gsd templates + * + * Handles the `/gsd start [template] [description]` and `/gsd templates` commands. + * Resolves templates by name or auto-detection, then dispatches the workflow prompt. + */ + +import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { + resolveByName, + autoDetect, + listTemplates, + getTemplateInfo, + loadWorkflowTemplate, + loadRegistry, + type TemplateMatch, +} from "./workflow-templates.js"; +import { loadPrompt } from "./prompt-loader.js"; +import { gsdRoot } from "./paths.js"; +import { GitServiceImpl, runGit } from "./git-service.js"; +import { loadEffectiveGSDPreferences } from "./preferences.js"; +import { isAutoActive, isAutoPaused } from "./auto.js"; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** + * Generate a URL-friendly slug from text. + */ +function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "") + .slice(0, 40) + .replace(/-$/, ""); +} + +/** + * Get the next workflow task number by scanning existing directories. + */ +function getNextWorkflowNum(workflowDir: string): number { + if (!existsSync(workflowDir)) return 1; + try { + const entries = readdirSync(workflowDir, { withFileTypes: true }); + let max = 0; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const match = entry.name.match(/^(\d{6})-(\d+)-/); + if (match) { + const num = parseInt(match[2], 10); + if (num > max) max = num; + } + } + return max + 1; + } catch { + return 1; + } +} + +/** + * Format the date as YYMMDD for directory naming. + */ +function datePrefix(): string { + const d = new Date(); + const yy = String(d.getFullYear()).slice(2); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return `${yy}${mm}${dd}`; +} + +// ─── State Types ───────────────────────────────────────────────────────────── + +interface WorkflowPhaseState { + name: string; + index: number; + status: "pending" | "active" | "completed"; +} + +interface WorkflowState { + template: string; + templateName: string; + description: string; + branch: string; + phases: WorkflowPhaseState[]; + currentPhase: number; + startedAt: string; + updatedAt: string; + completedAt?: string; + artifactDir: string; +} + +/** + * Write a STATE.json file to track workflow execution state. + */ +function writeWorkflowState( + artifactDir: string, + templateId: string, + templateName: string, + phases: string[], + description: string, + branch: string, +): void { + const statePath = join(artifactDir, "STATE.json"); + const state: WorkflowState = { + template: templateId, + templateName, + description, + branch, + phases: phases.map((p, i) => ({ + name: p, + index: i, + status: i === 0 ? "active" as const : "pending" as const, + })), + currentPhase: 0, + startedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + artifactDir, + }; + writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n"); +} + +/** + * Scan all workflow artifact directories for in-progress STATE.json files. + * Returns workflows that were started but not completed. + */ +function findInProgressWorkflows(basePath: string): WorkflowState[] { + const workflowsRoot = join(gsdRoot(basePath), "workflows"); + if (!existsSync(workflowsRoot)) return []; + + const results: WorkflowState[] = []; + try { + // Scan each category dir (bugfixes/, features/, spikes/, etc.) + for (const category of readdirSync(workflowsRoot, { withFileTypes: true })) { + if (!category.isDirectory()) continue; + const categoryDir = join(workflowsRoot, category.name); + + for (const workflow of readdirSync(categoryDir, { withFileTypes: true })) { + if (!workflow.isDirectory()) continue; + const statePath = join(categoryDir, workflow.name, "STATE.json"); + if (!existsSync(statePath)) continue; + + try { + const raw = readFileSync(statePath, "utf-8"); + const state = JSON.parse(raw) as WorkflowState; + if (!state.completedAt) { + results.push(state); + } + } catch { /* corrupted state file — skip */ } + } + } + } catch { /* workflows dir unreadable — skip */ } + + // Sort by most recently updated + results.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); + return results; +} + +// ─── /gsd start ────────────────────────────────────────────────────────────── + +export async function handleStart( + args: string, + ctx: ExtensionCommandContext, + pi: ExtensionAPI, +): Promise { + const trimmed = args.trim(); + + // /gsd start --list → same as /gsd templates + if (trimmed === "--list" || trimmed === "list") { + ctx.ui.notify(listTemplates(), "info"); + return; + } + + // ─── Auto-mode conflict guard ────────────────────────────────────────── + // Workflow templates dispatch their own messages and switch git branches, + // which would conflict with an active auto-mode dispatch loop. + if (isAutoActive()) { + ctx.ui.notify( + "Cannot start a workflow template while auto-mode is running.\n" + + "Run /gsd pause first, then /gsd start.", + "warning", + ); + return; + } + + if (isAutoPaused()) { + ctx.ui.notify( + "Auto-mode is paused. Starting a workflow template will run independently.\n" + + "The paused auto-mode session can be resumed later with /gsd auto.", + "info", + ); + } + + // ─── Resume detection ─────────────────────────────────────────────────── + // /gsd start --resume or /gsd start resume → resume in-progress workflow + if (trimmed === "--resume" || trimmed === "resume") { + const basePath = process.cwd(); + const inProgress = findInProgressWorkflows(basePath); + if (inProgress.length === 0) { + ctx.ui.notify("No in-progress workflows found.", "info"); + return; + } + + // Resume the most recent one + const wf = inProgress[0]; + const activePhase = wf.phases.find(p => p.status === "active"); + const completedCount = wf.phases.filter(p => p.status === "completed").length; + + ctx.ui.notify( + `Resuming: ${wf.templateName}\n` + + `Description: ${wf.description}\n` + + `Progress: ${completedCount}/${wf.phases.length} phases completed\n` + + `Current phase: ${activePhase?.name ?? "unknown"}\n` + + `Branch: ${wf.branch}\n` + + `Artifacts: ${wf.artifactDir}`, + "info", + ); + + const workflowContent = loadWorkflowTemplate(wf.template); + if (!workflowContent) { + ctx.ui.notify(`Template "${wf.template}" workflow file not found.`, "warning"); + return; + } + + const prompt = loadPrompt("workflow-start", { + templateId: wf.template, + templateName: wf.templateName, + templateDescription: `RESUMING — pick up from phase "${activePhase?.name ?? "unknown"}" (${completedCount}/${wf.phases.length} phases done)`, + phases: wf.phases.map(p => `${p.name}${p.status === "completed" ? " ✓" : p.status === "active" ? " ←" : ""}`).join(" → "), + complexity: "resume", + artifactDir: wf.artifactDir, + branch: wf.branch, + description: wf.description, + issueRef: "(none)", + date: new Date().toISOString().split("T")[0], + workflowContent, + }); + + pi.sendMessage( + { customType: "gsd-workflow-template", content: prompt, display: false }, + { triggerTurn: true }, + ); + return; + } + + // Show in-progress workflows when /gsd start is called with no args + if (!trimmed) { + const basePath = process.cwd(); + const inProgress = findInProgressWorkflows(basePath); + if (inProgress.length > 0) { + const wf = inProgress[0]; + const activePhase = wf.phases.find(p => p.status === "active"); + const completedCount = wf.phases.filter(p => p.status === "completed").length; + ctx.ui.notify( + `In-progress workflow found:\n` + + ` ${wf.templateName}: "${wf.description}"\n` + + ` Phase ${completedCount + 1}/${wf.phases.length}: ${activePhase?.name ?? "unknown"}\n\n` + + `Run /gsd start resume to continue it.\n`, + "info", + ); + } + } + + // /gsd start --dry-run