gsd-pi 2.29.0-dev.7612840 → 2.29.0-dev.77f06e2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +24 -17
  2. package/dist/resources/extensions/bg-shell/process-manager.ts +13 -0
  3. package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
  4. package/dist/resources/extensions/gsd/auto-post-unit.ts +6 -3
  5. package/dist/resources/extensions/gsd/auto-recovery.ts +16 -22
  6. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +7 -6
  7. package/dist/resources/extensions/gsd/commands-handlers.ts +20 -1
  8. package/dist/resources/extensions/gsd/commands-logs.ts +13 -14
  9. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +44 -14
  10. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
  11. package/dist/resources/extensions/gsd/commands.ts +53 -21
  12. package/dist/resources/extensions/gsd/json-persistence.ts +16 -1
  13. package/dist/resources/extensions/gsd/prompts/workflow-start.md +28 -0
  14. package/dist/resources/extensions/gsd/queue-order.ts +10 -11
  15. package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
  16. package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
  17. package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
  18. package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
  19. package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
  20. package/dist/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
  21. package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
  22. package/dist/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
  23. package/dist/resources/extensions/gsd/workflow-templates/registry.json +85 -0
  24. package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
  25. package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
  26. package/dist/resources/extensions/gsd/workflow-templates/spike.md +69 -0
  27. package/dist/resources/extensions/gsd/workflow-templates.ts +241 -0
  28. package/dist/resources/extensions/mcp-client/index.ts +459 -0
  29. package/package.json +1 -1
  30. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  31. package/packages/pi-coding-agent/dist/core/extensions/loader.js +13 -0
  32. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  33. package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -0
  34. package/src/resources/extensions/bg-shell/process-manager.ts +13 -0
  35. package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
  36. package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
  37. package/src/resources/extensions/gsd/auto-recovery.ts +16 -22
  38. package/src/resources/extensions/gsd/auto-worktree-sync.ts +7 -6
  39. package/src/resources/extensions/gsd/commands-handlers.ts +20 -1
  40. package/src/resources/extensions/gsd/commands-logs.ts +13 -14
  41. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +44 -14
  42. package/src/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
  43. package/src/resources/extensions/gsd/commands.ts +53 -21
  44. package/src/resources/extensions/gsd/json-persistence.ts +16 -1
  45. package/src/resources/extensions/gsd/prompts/workflow-start.md +28 -0
  46. package/src/resources/extensions/gsd/queue-order.ts +10 -11
  47. package/src/resources/extensions/gsd/session-status-io.ts +23 -41
  48. package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
  49. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
  50. package/src/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
  51. package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
  52. package/src/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
  53. package/src/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
  54. package/src/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
  55. package/src/resources/extensions/gsd/workflow-templates/registry.json +85 -0
  56. package/src/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
  57. package/src/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
  58. package/src/resources/extensions/gsd/workflow-templates/spike.md +69 -0
  59. package/src/resources/extensions/gsd/workflow-templates.ts +241 -0
  60. package/src/resources/extensions/mcp-client/index.ts +459 -0
  61. package/dist/resources/extensions/mcporter/index.ts +0 -525
  62. package/src/resources/extensions/mcporter/index.ts +0 -525
@@ -0,0 +1,544 @@
1
+ /**
2
+ * GSD Workflow Template Commands — /gsd start, /gsd templates
3
+ *
4
+ * Handles the `/gsd start [template] [description]` and `/gsd templates` commands.
5
+ * Resolves templates by name or auto-detection, then dispatches the workflow prompt.
6
+ */
7
+
8
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
9
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import {
12
+ resolveByName,
13
+ autoDetect,
14
+ listTemplates,
15
+ getTemplateInfo,
16
+ loadWorkflowTemplate,
17
+ loadRegistry,
18
+ type TemplateMatch,
19
+ } from "./workflow-templates.js";
20
+ import { loadPrompt } from "./prompt-loader.js";
21
+ import { gsdRoot } from "./paths.js";
22
+ import { GitServiceImpl, runGit } from "./git-service.js";
23
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
24
+ import { isAutoActive, isAutoPaused } from "./auto.js";
25
+
26
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Generate a URL-friendly slug from text.
30
+ */
31
+ function slugify(text: string): string {
32
+ return text
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9]+/g, "-")
35
+ .replace(/^-|-$/g, "")
36
+ .slice(0, 40)
37
+ .replace(/-$/, "");
38
+ }
39
+
40
+ /**
41
+ * Get the next workflow task number by scanning existing directories.
42
+ */
43
+ function getNextWorkflowNum(workflowDir: string): number {
44
+ if (!existsSync(workflowDir)) return 1;
45
+ try {
46
+ const entries = readdirSync(workflowDir, { withFileTypes: true });
47
+ let max = 0;
48
+ for (const entry of entries) {
49
+ if (!entry.isDirectory()) continue;
50
+ const match = entry.name.match(/^(\d{6})-(\d+)-/);
51
+ if (match) {
52
+ const num = parseInt(match[2], 10);
53
+ if (num > max) max = num;
54
+ }
55
+ }
56
+ return max + 1;
57
+ } catch {
58
+ return 1;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Format the date as YYMMDD for directory naming.
64
+ */
65
+ function datePrefix(): string {
66
+ const d = new Date();
67
+ const yy = String(d.getFullYear()).slice(2);
68
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
69
+ const dd = String(d.getDate()).padStart(2, "0");
70
+ return `${yy}${mm}${dd}`;
71
+ }
72
+
73
+ // ─── State Types ─────────────────────────────────────────────────────────────
74
+
75
+ interface WorkflowPhaseState {
76
+ name: string;
77
+ index: number;
78
+ status: "pending" | "active" | "completed";
79
+ }
80
+
81
+ interface WorkflowState {
82
+ template: string;
83
+ templateName: string;
84
+ description: string;
85
+ branch: string;
86
+ phases: WorkflowPhaseState[];
87
+ currentPhase: number;
88
+ startedAt: string;
89
+ updatedAt: string;
90
+ completedAt?: string;
91
+ artifactDir: string;
92
+ }
93
+
94
+ /**
95
+ * Write a STATE.json file to track workflow execution state.
96
+ */
97
+ function writeWorkflowState(
98
+ artifactDir: string,
99
+ templateId: string,
100
+ templateName: string,
101
+ phases: string[],
102
+ description: string,
103
+ branch: string,
104
+ ): void {
105
+ const statePath = join(artifactDir, "STATE.json");
106
+ const state: WorkflowState = {
107
+ template: templateId,
108
+ templateName,
109
+ description,
110
+ branch,
111
+ phases: phases.map((p, i) => ({
112
+ name: p,
113
+ index: i,
114
+ status: i === 0 ? "active" as const : "pending" as const,
115
+ })),
116
+ currentPhase: 0,
117
+ startedAt: new Date().toISOString(),
118
+ updatedAt: new Date().toISOString(),
119
+ artifactDir,
120
+ };
121
+ writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
122
+ }
123
+
124
+ /**
125
+ * Scan all workflow artifact directories for in-progress STATE.json files.
126
+ * Returns workflows that were started but not completed.
127
+ */
128
+ function findInProgressWorkflows(basePath: string): WorkflowState[] {
129
+ const workflowsRoot = join(gsdRoot(basePath), "workflows");
130
+ if (!existsSync(workflowsRoot)) return [];
131
+
132
+ const results: WorkflowState[] = [];
133
+ try {
134
+ // Scan each category dir (bugfixes/, features/, spikes/, etc.)
135
+ for (const category of readdirSync(workflowsRoot, { withFileTypes: true })) {
136
+ if (!category.isDirectory()) continue;
137
+ const categoryDir = join(workflowsRoot, category.name);
138
+
139
+ for (const workflow of readdirSync(categoryDir, { withFileTypes: true })) {
140
+ if (!workflow.isDirectory()) continue;
141
+ const statePath = join(categoryDir, workflow.name, "STATE.json");
142
+ if (!existsSync(statePath)) continue;
143
+
144
+ try {
145
+ const raw = readFileSync(statePath, "utf-8");
146
+ const state = JSON.parse(raw) as WorkflowState;
147
+ if (!state.completedAt) {
148
+ results.push(state);
149
+ }
150
+ } catch { /* corrupted state file — skip */ }
151
+ }
152
+ }
153
+ } catch { /* workflows dir unreadable — skip */ }
154
+
155
+ // Sort by most recently updated
156
+ results.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
157
+ return results;
158
+ }
159
+
160
+ // ─── /gsd start ──────────────────────────────────────────────────────────────
161
+
162
+ export async function handleStart(
163
+ args: string,
164
+ ctx: ExtensionCommandContext,
165
+ pi: ExtensionAPI,
166
+ ): Promise<void> {
167
+ const trimmed = args.trim();
168
+
169
+ // /gsd start --list → same as /gsd templates
170
+ if (trimmed === "--list" || trimmed === "list") {
171
+ ctx.ui.notify(listTemplates(), "info");
172
+ return;
173
+ }
174
+
175
+ // ─── Auto-mode conflict guard ──────────────────────────────────────────
176
+ // Workflow templates dispatch their own messages and switch git branches,
177
+ // which would conflict with an active auto-mode dispatch loop.
178
+ if (isAutoActive()) {
179
+ ctx.ui.notify(
180
+ "Cannot start a workflow template while auto-mode is running.\n" +
181
+ "Run /gsd pause first, then /gsd start.",
182
+ "warning",
183
+ );
184
+ return;
185
+ }
186
+
187
+ if (isAutoPaused()) {
188
+ ctx.ui.notify(
189
+ "Auto-mode is paused. Starting a workflow template will run independently.\n" +
190
+ "The paused auto-mode session can be resumed later with /gsd auto.",
191
+ "info",
192
+ );
193
+ }
194
+
195
+ // ─── Resume detection ───────────────────────────────────────────────────
196
+ // /gsd start --resume or /gsd start resume → resume in-progress workflow
197
+ if (trimmed === "--resume" || trimmed === "resume") {
198
+ const basePath = process.cwd();
199
+ const inProgress = findInProgressWorkflows(basePath);
200
+ if (inProgress.length === 0) {
201
+ ctx.ui.notify("No in-progress workflows found.", "info");
202
+ return;
203
+ }
204
+
205
+ // Resume the most recent one
206
+ const wf = inProgress[0];
207
+ const activePhase = wf.phases.find(p => p.status === "active");
208
+ const completedCount = wf.phases.filter(p => p.status === "completed").length;
209
+
210
+ ctx.ui.notify(
211
+ `Resuming: ${wf.templateName}\n` +
212
+ `Description: ${wf.description}\n` +
213
+ `Progress: ${completedCount}/${wf.phases.length} phases completed\n` +
214
+ `Current phase: ${activePhase?.name ?? "unknown"}\n` +
215
+ `Branch: ${wf.branch}\n` +
216
+ `Artifacts: ${wf.artifactDir}`,
217
+ "info",
218
+ );
219
+
220
+ const workflowContent = loadWorkflowTemplate(wf.template);
221
+ if (!workflowContent) {
222
+ ctx.ui.notify(`Template "${wf.template}" workflow file not found.`, "warning");
223
+ return;
224
+ }
225
+
226
+ const prompt = loadPrompt("workflow-start", {
227
+ templateId: wf.template,
228
+ templateName: wf.templateName,
229
+ templateDescription: `RESUMING — pick up from phase "${activePhase?.name ?? "unknown"}" (${completedCount}/${wf.phases.length} phases done)`,
230
+ phases: wf.phases.map(p => `${p.name}${p.status === "completed" ? " ✓" : p.status === "active" ? " ←" : ""}`).join(" → "),
231
+ complexity: "resume",
232
+ artifactDir: wf.artifactDir,
233
+ branch: wf.branch,
234
+ description: wf.description,
235
+ issueRef: "(none)",
236
+ date: new Date().toISOString().split("T")[0],
237
+ workflowContent,
238
+ });
239
+
240
+ pi.sendMessage(
241
+ { customType: "gsd-workflow-template", content: prompt, display: false },
242
+ { triggerTurn: true },
243
+ );
244
+ return;
245
+ }
246
+
247
+ // Show in-progress workflows when /gsd start is called with no args
248
+ if (!trimmed) {
249
+ const basePath = process.cwd();
250
+ const inProgress = findInProgressWorkflows(basePath);
251
+ if (inProgress.length > 0) {
252
+ const wf = inProgress[0];
253
+ const activePhase = wf.phases.find(p => p.status === "active");
254
+ const completedCount = wf.phases.filter(p => p.status === "completed").length;
255
+ ctx.ui.notify(
256
+ `In-progress workflow found:\n` +
257
+ ` ${wf.templateName}: "${wf.description}"\n` +
258
+ ` Phase ${completedCount + 1}/${wf.phases.length}: ${activePhase?.name ?? "unknown"}\n\n` +
259
+ `Run /gsd start resume to continue it.\n`,
260
+ "info",
261
+ );
262
+ }
263
+ }
264
+
265
+ // /gsd start --dry-run <template> → preview without executing
266
+ const dryRun = trimmed.includes("--dry-run");
267
+ const cleanedArgs = trimmed.replace(/--dry-run\s*/, "").trim();
268
+
269
+ // Parse: first word might be a template name, rest is description
270
+ const parts = cleanedArgs.split(/\s+/);
271
+ const firstWord = parts[0] ?? "";
272
+
273
+ // Check for --issue flag (bugfix shortcut)
274
+ const issueMatch = cleanedArgs.match(/--issue\s+(\S+)/);
275
+ const issueRef = issueMatch ? issueMatch[1] : null;
276
+
277
+ // Try resolving first word as a template name
278
+ let match: TemplateMatch | null = null;
279
+ let description = "";
280
+
281
+ if (firstWord) {
282
+ match = resolveByName(firstWord);
283
+ if (match) {
284
+ // First word was a template name; rest is description
285
+ description = parts.slice(1).join(" ").replace(/--issue\s+\S+/, "").trim();
286
+ }
287
+ }
288
+
289
+ // If no explicit template, try auto-detection from the full input
290
+ if (!match && cleanedArgs) {
291
+ const detected = autoDetect(cleanedArgs);
292
+ if (detected.length === 1 || (detected.length > 0 && detected[0].confidence === "high")) {
293
+ match = detected[0];
294
+ description = cleanedArgs;
295
+ ctx.ui.notify(
296
+ `Auto-detected template: ${match.template.name} (matched: "${match.matchedTrigger}")`,
297
+ "info",
298
+ );
299
+ } else if (detected.length > 1) {
300
+ const choices = detected.slice(0, 4).map(
301
+ (m) => ` /gsd start ${m.id} ${cleanedArgs}`
302
+ );
303
+ ctx.ui.notify(
304
+ `Multiple templates could match. Pick one:\n\n${choices.join("\n")}\n\nOr specify explicitly: /gsd start <template> <description>`,
305
+ "info",
306
+ );
307
+ return;
308
+ }
309
+ }
310
+
311
+ // No template resolved at all
312
+ if (!match) {
313
+ if (!trimmed) {
314
+ ctx.ui.notify(
315
+ "Usage: /gsd start <template> [description]\n\n" +
316
+ "Templates:\n" +
317
+ " bugfix Triage → fix → verify → ship\n" +
318
+ " small-feature Scope → plan → implement → verify\n" +
319
+ " spike Scope → research → synthesize\n" +
320
+ " hotfix Fix → ship (minimal ceremony)\n" +
321
+ " refactor Inventory → plan → migrate → verify\n" +
322
+ " security-audit Scan → triage → remediate → re-scan\n" +
323
+ " dep-upgrade Assess → upgrade → fix → verify\n" +
324
+ " full-project Complete GSD with full ceremony\n\n" +
325
+ "Examples:\n" +
326
+ " /gsd start bugfix fix login button not responding\n" +
327
+ " /gsd start spike evaluate auth libraries\n" +
328
+ " /gsd start hotfix critical: API returns 500\n\n" +
329
+ "Flags:\n" +
330
+ " --dry-run Preview what would happen without executing\n" +
331
+ " --issue <ref> Link to a GitHub issue\n\n" +
332
+ "Run /gsd templates for detailed template info.",
333
+ "info",
334
+ );
335
+ } else {
336
+ ctx.ui.notify(
337
+ `No template matched "${firstWord}". Run /gsd start to see available templates.`,
338
+ "warning",
339
+ );
340
+ }
341
+ return;
342
+ }
343
+
344
+ // ─── Resolved template ───────────────────────────────────────────────────
345
+
346
+ const templateId = match.id;
347
+ const template = match.template;
348
+ const basePath = process.cwd();
349
+ const date = new Date().toISOString().split("T")[0];
350
+
351
+ // Load the workflow template content
352
+ const workflowContent = loadWorkflowTemplate(templateId);
353
+ if (!workflowContent) {
354
+ ctx.ui.notify(
355
+ `Template "${templateId}" is registered but its workflow file (${template.file}) hasn't been created yet.`,
356
+ "warning",
357
+ );
358
+ return;
359
+ }
360
+
361
+ // ─── Dry-run mode: preview without executing ────────────────────────────
362
+
363
+ if (dryRun) {
364
+ const slug = slugify(description || templateId);
365
+ const lines = [
366
+ `DRY RUN — ${template.name} (${templateId})\n`,
367
+ `Description: ${description || "(none)"}`,
368
+ `Complexity: ${template.estimated_complexity}`,
369
+ `Phases: ${template.phases.join(" → ")}`,
370
+ "",
371
+ ];
372
+ if (template.artifact_dir) {
373
+ const prefix = datePrefix();
374
+ const num = getNextWorkflowNum(join(basePath, template.artifact_dir));
375
+ lines.push(`Artifact dir: ${template.artifact_dir}${prefix}-${num}-${slug}`);
376
+ } else {
377
+ lines.push("Artifact dir: (none — hotfix mode)");
378
+ }
379
+ lines.push(`Branch: gsd/${templateId}/${slug}`);
380
+ if (issueRef) lines.push(`Issue: ${issueRef}`);
381
+ lines.push("", "No changes made. Remove --dry-run to execute.");
382
+ ctx.ui.notify(lines.join("\n"), "info");
383
+ return;
384
+ }
385
+
386
+ // ─── Route full-project to standard GSD workflow ────────────────────────
387
+
388
+ if (templateId === "full-project") {
389
+ const root = gsdRoot(basePath);
390
+ if (!existsSync(root)) {
391
+ ctx.ui.notify(
392
+ "Routing to /gsd init for full project setup...",
393
+ "info",
394
+ );
395
+ // Trigger /gsd init by dispatching to the handler
396
+ pi.sendMessage(
397
+ {
398
+ customType: "gsd-workflow-template",
399
+ content: "The user wants to start a full GSD project. Run `/gsd init` to bootstrap the project, then `/gsd auto` to begin execution.",
400
+ display: false,
401
+ },
402
+ { triggerTurn: true },
403
+ );
404
+ } else {
405
+ ctx.ui.notify(
406
+ "Project already initialized. Use `/gsd auto` to continue or `/gsd discuss` to start a new milestone.",
407
+ "info",
408
+ );
409
+ }
410
+ return;
411
+ }
412
+
413
+ // ─── Create artifact directory ──────────────────────────────────────────
414
+
415
+ let artifactDir = "";
416
+ if (template.artifact_dir) {
417
+ const slug = slugify(description || templateId);
418
+ const prefix = datePrefix();
419
+ const num = getNextWorkflowNum(join(basePath, template.artifact_dir));
420
+ artifactDir = `${template.artifact_dir}${prefix}-${num}-${slug}`;
421
+ mkdirSync(join(basePath, artifactDir), { recursive: true });
422
+ }
423
+
424
+ // ─── Create git branch (unless isolation: none) ─────────────────────────
425
+
426
+ const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
427
+ const git = new GitServiceImpl(basePath, gitPrefs);
428
+ const skipBranch = gitPrefs.isolation === "none";
429
+ const slug = slugify(description || templateId);
430
+ const branchName = `gsd/${templateId}/${slug}`;
431
+ let branchCreated = false;
432
+
433
+ if (!skipBranch) {
434
+ try {
435
+ const current = git.getCurrentBranch();
436
+ if (current !== branchName) {
437
+ try {
438
+ git.autoCommit("workflow-template", templateId, []);
439
+ } catch { /* nothing to commit */ }
440
+ runGit(basePath, ["checkout", "-b", branchName]);
441
+ branchCreated = true;
442
+ }
443
+ } catch (err) {
444
+ const message = err instanceof Error ? err.message : String(err);
445
+ ctx.ui.notify(
446
+ `Could not create branch ${branchName}: ${message}. Working on current branch.`,
447
+ "warning",
448
+ );
449
+ }
450
+ }
451
+
452
+ const actualBranch = branchCreated ? branchName : git.getCurrentBranch();
453
+
454
+ // ─── Write workflow state for resume support ────────────────────────────
455
+
456
+ if (artifactDir) {
457
+ writeWorkflowState(
458
+ join(basePath, artifactDir),
459
+ templateId,
460
+ template.name,
461
+ template.phases,
462
+ description,
463
+ actualBranch,
464
+ );
465
+ }
466
+
467
+ // ─── Notify and dispatch ────────────────────────────────────────────────
468
+
469
+ const infoLines = [
470
+ `Starting workflow: ${template.name}`,
471
+ `Phases: ${template.phases.join(" → ")}`,
472
+ ];
473
+ if (artifactDir) infoLines.push(`Artifacts: ${artifactDir}`);
474
+ infoLines.push(`Branch: ${actualBranch}`);
475
+ ctx.ui.notify(infoLines.join("\n"), "info");
476
+
477
+ const prompt = loadPrompt("workflow-start", {
478
+ templateId,
479
+ templateName: template.name,
480
+ templateDescription: template.description,
481
+ phases: template.phases.join(" → "),
482
+ complexity: template.estimated_complexity,
483
+ artifactDir: artifactDir || "(none)",
484
+ branch: actualBranch,
485
+ description: description || "(none provided)",
486
+ issueRef: issueRef || "(none)",
487
+ date,
488
+ workflowContent,
489
+ });
490
+
491
+ pi.sendMessage(
492
+ {
493
+ customType: "gsd-workflow-template",
494
+ content: prompt,
495
+ display: false,
496
+ },
497
+ { triggerTurn: true },
498
+ );
499
+ }
500
+
501
+ // ─── /gsd templates ──────────────────────────────────────────────────────────
502
+
503
+ export async function handleTemplates(
504
+ args: string,
505
+ ctx: ExtensionCommandContext,
506
+ ): Promise<void> {
507
+ const trimmed = args.trim();
508
+
509
+ // /gsd templates info <name>
510
+ if (trimmed.startsWith("info ")) {
511
+ const name = trimmed.replace(/^info\s+/, "").trim();
512
+ const info = getTemplateInfo(name);
513
+ if (info) {
514
+ ctx.ui.notify(info, "info");
515
+ } else {
516
+ ctx.ui.notify(
517
+ `Unknown template "${name}". Run /gsd templates to see available templates.`,
518
+ "warning",
519
+ );
520
+ }
521
+ return;
522
+ }
523
+
524
+ // /gsd templates — list all
525
+ ctx.ui.notify(listTemplates(), "info");
526
+ }
527
+
528
+ /**
529
+ * Return template IDs for autocomplete in /gsd templates info <name>.
530
+ */
531
+ export function getTemplateCompletions(prefix: string): Array<{ value: string; label: string; description: string }> {
532
+ try {
533
+ const registry = loadRegistry();
534
+ return Object.entries(registry.templates)
535
+ .filter(([id]) => id.startsWith(prefix))
536
+ .map(([id, entry]) => ({
537
+ value: `info ${id}`,
538
+ label: id,
539
+ description: entry.description,
540
+ }));
541
+ } catch {
542
+ return [];
543
+ }
544
+ }
@@ -22,8 +22,6 @@ import {
22
22
  getProjectGSDPreferencesPath,
23
23
  loadEffectiveGSDPreferences,
24
24
  } from "./preferences.js";
25
- import { loadPrompt } from "./prompt-loader.js";
26
-
27
25
  import { handleRemote } from "../remote-questions/mod.js";
28
26
  import { handleQuick } from "./quick.js";
29
27
  import { handleHistory } from "./history.js";
@@ -45,26 +43,9 @@ import { handleInspect } from "./commands-inspect.js";
45
43
  import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
46
44
  import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
47
45
  import { handleLogs } from "./commands-logs.js";
46
+ import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
48
47
 
49
48
 
50
- export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
51
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
52
- const workflow = readFileSync(workflowPath, "utf-8");
53
- const prompt = loadPrompt("doctor-heal", {
54
- doctorSummary: reportText,
55
- structuredIssues,
56
- scopeLabel: scope ?? "active milestone / blocking scope",
57
- doctorCommandSuffix: scope ? ` ${scope}` : "",
58
- });
59
-
60
- const content = `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`;
61
-
62
- pi.sendMessage(
63
- { customType: "gsd-doctor-heal", content, display: false },
64
- { triggerTurn: true },
65
- );
66
- }
67
-
68
49
  /** Resolve the effective project root, accounting for worktree paths. */
69
50
  export function projectRoot(): string {
70
51
  const root = resolveProjectRoot(process.cwd());
@@ -74,7 +55,7 @@ export function projectRoot(): string {
74
55
 
75
56
  export function registerGSDCommand(pi: ExtensionAPI): void {
76
57
  pi.registerCommand("gsd", {
77
- description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
58
+ description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
78
59
  getArgumentCompletions: (prefix: string) => {
79
60
  const subcommands = [
80
61
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -117,6 +98,8 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
117
98
  { cmd: "park", desc: "Park a milestone — skip without deleting" },
118
99
  { cmd: "unpark", desc: "Reactivate a parked milestone" },
119
100
  { cmd: "update", desc: "Update GSD to the latest version" },
101
+ { cmd: "start", desc: "Start a workflow template (bugfix, spike, feature, etc.)" },
102
+ { cmd: "templates", desc: "List available workflow templates" },
120
103
  ];
121
104
  const parts = prefix.trim().split(/\s+/);
122
105
 
@@ -302,6 +285,42 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
302
285
  .map((s) => ({ value: `knowledge ${s.cmd}`, label: s.cmd, description: s.desc }));
303
286
  }
304
287
 
288
+ if (parts[0] === "start" && parts.length <= 2) {
289
+ const subPrefix = parts[1] ?? "";
290
+ const subs = [
291
+ { cmd: "bugfix", desc: "Triage, fix, test, and ship a bug fix" },
292
+ { cmd: "small-feature", desc: "Lightweight feature with optional discussion" },
293
+ { cmd: "spike", desc: "Research, prototype, and document findings" },
294
+ { cmd: "hotfix", desc: "Minimal: fix it, test it, ship it" },
295
+ { cmd: "refactor", desc: "Inventory, plan waves, migrate, verify" },
296
+ { cmd: "security-audit", desc: "Scan, triage, remediate, re-scan" },
297
+ { cmd: "dep-upgrade", desc: "Assess, upgrade, fix breaks, verify" },
298
+ { cmd: "full-project", desc: "Complete GSD workflow with full ceremony" },
299
+ { cmd: "resume", desc: "Resume an in-progress workflow" },
300
+ { cmd: "--list", desc: "List all available templates" },
301
+ { cmd: "--dry-run", desc: "Preview workflow without executing" },
302
+ ];
303
+ return subs
304
+ .filter((s) => s.cmd.startsWith(subPrefix))
305
+ .map((s) => ({ value: `start ${s.cmd}`, label: s.cmd, description: s.desc }));
306
+ }
307
+
308
+ if (parts[0] === "templates" && parts.length <= 2) {
309
+ const subPrefix = parts[1] ?? "";
310
+ const subs = [
311
+ { cmd: "info", desc: "Show detailed template info" },
312
+ ];
313
+ return subs
314
+ .filter((s) => s.cmd.startsWith(subPrefix))
315
+ .map((s) => ({ value: `templates ${s.cmd}`, label: s.cmd, description: s.desc }));
316
+ }
317
+
318
+ if (parts[0] === "templates" && parts[1] === "info" && parts.length <= 3) {
319
+ const namePrefix = parts[2] ?? "";
320
+ return getTemplateCompletions(namePrefix)
321
+ .map((c) => ({ value: `templates ${c.value}`, label: c.label, description: c.description }));
322
+ }
323
+
305
324
  if (parts[0] === "doctor") {
306
325
  const modePrefix = parts[1] ?? "";
307
326
  const modes = [
@@ -793,6 +812,17 @@ Examples:
793
812
  return;
794
813
  }
795
814
 
815
+ // ─── Workflow Templates ────────────────────────────────────────
816
+ if (trimmed === "start" || trimmed.startsWith("start ")) {
817
+ await handleStart(trimmed.replace(/^start\s*/, "").trim(), ctx, pi);
818
+ return;
819
+ }
820
+
821
+ if (trimmed === "templates" || trimmed.startsWith("templates ")) {
822
+ await handleTemplates(trimmed.replace(/^templates\s*/, "").trim(), ctx);
823
+ return;
824
+ }
825
+
796
826
  if (trimmed === "") {
797
827
  // Bare /gsd defaults to step mode
798
828
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
@@ -811,6 +841,8 @@ function showHelp(ctx: ExtensionCommandContext): void {
811
841
  const lines = [
812
842
  "GSD — Get Shit Done\n",
813
843
  "WORKFLOW",
844
+ " /gsd start <tpl> Start a workflow template (bugfix, spike, feature, hotfix, etc.)",
845
+ " /gsd templates List available workflow templates [info <name>]",
814
846
  " /gsd Run next unit in step mode (same as /gsd next)",
815
847
  " /gsd next Execute next task, then pause [--dry-run] [--verbose]",
816
848
  " /gsd auto Run all queued units continuously [--verbose]",
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
 
4
4
  /**
@@ -50,3 +50,18 @@ export function saveJsonFile<T>(filePath: string, data: T): void {
50
50
  // Non-fatal — don't let persistence failures break operation
51
51
  }
52
52
  }
53
+
54
+ /**
55
+ * Write a JSON file atomically (write to .tmp, then rename).
56
+ * Creates parent directories as needed. Non-fatal on error.
57
+ */
58
+ export function writeJsonFileAtomic<T>(filePath: string, data: T): void {
59
+ try {
60
+ mkdirSync(dirname(filePath), { recursive: true });
61
+ const tmp = filePath + ".tmp";
62
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
63
+ renameSync(tmp, filePath);
64
+ } catch {
65
+ // Non-fatal — don't let persistence failures break operation
66
+ }
67
+ }