gsd-pi 0.2.9 → 0.3.0

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 (29) hide show
  1. package/README.md +23 -0
  2. package/dist/cli.js +47 -5
  3. package/dist/wizard.js +2 -1
  4. package/package.json +1 -1
  5. package/src/resources/extensions/gsd/commands.ts +9 -3
  6. package/src/resources/extensions/gsd/dashboard-overlay.ts +6 -1
  7. package/src/resources/extensions/gsd/files.ts +7 -7
  8. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  9. package/src/resources/extensions/gsd/index.ts +36 -1
  10. package/src/resources/extensions/gsd/migrate/command.ts +215 -0
  11. package/src/resources/extensions/gsd/migrate/index.ts +42 -0
  12. package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
  13. package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
  14. package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
  15. package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
  16. package/src/resources/extensions/gsd/migrate/types.ts +370 -0
  17. package/src/resources/extensions/gsd/migrate/validator.ts +53 -0
  18. package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
  19. package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
  20. package/src/resources/extensions/gsd/prompts/worktree-merge.md +89 -0
  21. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
  22. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
  23. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
  24. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
  25. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
  26. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
  27. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
  28. package/src/resources/extensions/gsd/worktree-command.ts +527 -0
  29. package/src/resources/extensions/gsd/worktree-manager.ts +302 -0
package/README.md CHANGED
@@ -46,6 +46,28 @@ GSD v2 solves all of these because it's not a prompt framework anymore — it's
46
46
  | Roadmap reassessment | Manual | Automatic after each slice completes |
47
47
  | Skill discovery | None | Auto-detect and install relevant skills during research |
48
48
 
49
+ ### Migrating from v1
50
+
51
+ If you have projects with `.planning` directories from the original Get Shit Done, you can migrate them to GSD-2's `.gsd` format:
52
+
53
+ ```bash
54
+ # From within the project directory
55
+ /gsd migrate
56
+
57
+ # Or specify a path
58
+ /gsd migrate ~/projects/my-old-project
59
+ ```
60
+
61
+ The migration tool:
62
+ - Parses your old `PROJECT.md`, `ROADMAP.md`, `REQUIREMENTS.md`, phase directories, plans, summaries, and research
63
+ - Maps phases → slices, plans → tasks, milestones → milestones
64
+ - Preserves completion state (`[x]` phases stay done, summaries carry over)
65
+ - Consolidates research files into the new structure
66
+ - Shows a preview before writing anything
67
+ - Optionally runs an agent-driven review of the output for quality assurance
68
+
69
+ Supports format variations including milestone-sectioned roadmaps with `<details>` blocks, bold phase entries, bullet-format requirements, decimal phase numbering, and duplicate phase numbers across milestones.
70
+
49
71
  ---
50
72
 
51
73
  ## How It Works
@@ -187,6 +209,7 @@ On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) f
187
209
  | `/gsd status` | Progress dashboard |
188
210
  | `/gsd queue` | Queue future milestones (safe during auto mode) |
189
211
  | `/gsd prefs` | Model selection, timeouts, budget ceiling |
212
+ | `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format |
190
213
  | `/gsd doctor` | Validate `.gsd/` integrity, find and fix issues |
191
214
  | `Ctrl+Alt+G` | Toggle dashboard overlay |
192
215
 
package/dist/cli.js CHANGED
@@ -7,18 +7,19 @@ loadStoredEnvKeys(authStorage);
7
7
  await runWizardIfNeeded(authStorage);
8
8
  const modelRegistry = new ModelRegistry(authStorage);
9
9
  const settingsManager = SettingsManager.create(agentDir);
10
- // Always ensure defaults: anthropic/claude-sonnet-4-6, thinking off.
11
- // Validates on every startup — catches stale settings from prior installs
10
+ // Validate configured model on startup — catches stale settings from prior installs
12
11
  // (e.g. grok-2 which no longer exists) and fresh installs with no settings.
12
+ // Only resets the default when the configured model no longer exists in the registry;
13
+ // never overwrites a valid user choice.
13
14
  const configuredProvider = settingsManager.getDefaultProvider();
14
15
  const configuredModel = settingsManager.getDefaultModel();
15
16
  const allModels = modelRegistry.getAll();
16
17
  const configuredExists = configuredProvider && configuredModel &&
17
18
  allModels.some((m) => m.provider === configuredProvider && m.id === configuredModel);
18
19
  if (!configuredModel || !configuredExists) {
19
- // Preferred default: anthropic/claude-sonnet-4-6
20
- const preferred = allModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-sonnet-4-6') ||
21
- allModels.find((m) => m.provider === 'anthropic' && m.id.includes('sonnet')) ||
20
+ // Fallback: pick the best available Anthropic model
21
+ const preferred = allModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-opus-4-6') ||
22
+ allModels.find((m) => m.provider === 'anthropic' && m.id.includes('opus')) ||
22
23
  allModels.find((m) => m.provider === 'anthropic');
23
24
  if (preferred) {
24
25
  settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id);
@@ -52,5 +53,46 @@ if (extensionsResult.errors.length > 0) {
52
53
  process.stderr.write(`[gsd] Extension load error: ${err.error}\n`);
53
54
  }
54
55
  }
56
+ // Restore scoped models from settings on startup.
57
+ // The upstream InteractiveMode reads enabledModels from settings when /scoped-models is opened,
58
+ // but doesn't apply them to the session at startup — so Ctrl+P cycles all models instead of
59
+ // just the saved selection until the user re-runs /scoped-models.
60
+ const enabledModelPatterns = settingsManager.getEnabledModels();
61
+ if (enabledModelPatterns && enabledModelPatterns.length > 0) {
62
+ const availableModels = modelRegistry.getAvailable();
63
+ const scopedModels = [];
64
+ const seen = new Set();
65
+ for (const pattern of enabledModelPatterns) {
66
+ // Patterns are "provider/modelId" exact strings saved by /scoped-models
67
+ const slashIdx = pattern.indexOf('/');
68
+ if (slashIdx !== -1) {
69
+ const provider = pattern.substring(0, slashIdx);
70
+ const modelId = pattern.substring(slashIdx + 1);
71
+ const model = availableModels.find((m) => m.provider === provider && m.id === modelId);
72
+ if (model) {
73
+ const key = `${model.provider}/${model.id}`;
74
+ if (!seen.has(key)) {
75
+ seen.add(key);
76
+ scopedModels.push({ model });
77
+ }
78
+ }
79
+ }
80
+ else {
81
+ // Fallback: match by model id alone
82
+ const model = availableModels.find((m) => m.id === pattern);
83
+ if (model) {
84
+ const key = `${model.provider}/${model.id}`;
85
+ if (!seen.has(key)) {
86
+ seen.add(key);
87
+ scopedModels.push({ model });
88
+ }
89
+ }
90
+ }
91
+ }
92
+ // Only apply if we resolved some models and it's a genuine subset
93
+ if (scopedModels.length > 0 && scopedModels.length < availableModels.length) {
94
+ session.setScopedModels(scopedModels);
95
+ }
96
+ }
55
97
  const interactiveMode = new InteractiveMode(session);
56
98
  await interactiveMode.run();
package/dist/wizard.js CHANGED
@@ -75,7 +75,7 @@ export function loadStoredEnvKeys(authStorage) {
75
75
  for (const [provider, envVar] of providers) {
76
76
  if (!process.env[envVar]) {
77
77
  const cred = authStorage.get(provider);
78
- if (cred?.type === 'api_key') {
78
+ if (cred?.type === 'api_key' && cred.key) {
79
79
  process.env[envVar] = cred.key;
80
80
  }
81
81
  }
@@ -143,6 +143,7 @@ export async function runWizardIfNeeded(authStorage) {
143
143
  savedCount++;
144
144
  }
145
145
  else {
146
+ authStorage.set(key.provider, { type: 'api_key', key: '' });
146
147
  process.stdout.write(` ${dim}↷ ${key.label} skipped${reset}\n\n`);
147
148
  }
148
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "GSD — Get Stuff Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,6 +30,7 @@ import {
30
30
  filterDoctorIssues,
31
31
  } from "./doctor.js";
32
32
  import { loadPrompt } from "./prompt-loader.js";
33
+ import { handleMigrate } from "./migrate/command.js";
33
34
 
34
35
  function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
35
36
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
@@ -51,10 +52,10 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
51
52
 
52
53
  export function registerGSDCommand(pi: ExtensionAPI): void {
53
54
  pi.registerCommand("gsd", {
54
- description: "GSD — Get Stuff Done: /gsd auto|stop|status|queue|prefs|doctor",
55
+ description: "GSD — Get Stuff Done: /gsd auto|stop|status|queue|prefs|doctor|migrate",
55
56
 
56
57
  getArgumentCompletions: (prefix: string) => {
57
- const subcommands = ["auto", "stop", "status", "queue", "discuss", "prefs", "doctor"];
58
+ const subcommands = ["auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate"];
58
59
  const parts = prefix.trim().split(/\s+/);
59
60
 
60
61
  if (parts.length <= 1) {
@@ -136,13 +137,18 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
136
137
  return;
137
138
  }
138
139
 
140
+ if (trimmed === "migrate" || trimmed.startsWith("migrate ")) {
141
+ await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi);
142
+ return;
143
+ }
144
+
139
145
  if (trimmed === "") {
140
146
  await showSmartEntry(ctx, pi, process.cwd());
141
147
  return;
142
148
  }
143
149
 
144
150
  ctx.ui.notify(
145
- `Unknown: /gsd ${trimmed}. Use /gsd, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], or /gsd doctor [audit|fix|heal] [M###/S##].`,
151
+ `Unknown: /gsd ${trimmed}. Use /gsd, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], /gsd doctor [audit|fix|heal] [M###/S##], or /gsd migrate <path>.`,
146
152
  "warning",
147
153
  );
148
154
  },
@@ -17,6 +17,7 @@ import {
17
17
  aggregateByModel, formatCost, formatTokenCount, formatCostProjection,
18
18
  } from "./metrics.js";
19
19
  import { loadEffectiveGSDPreferences } from "./preferences.js";
20
+ import { getActiveWorktreeName } from "./worktree-command.js";
20
21
 
21
22
  function formatDuration(ms: number): string {
22
23
  const s = Math.floor(ms / 1000);
@@ -273,8 +274,12 @@ export class GSDDashboardOverlay {
273
274
  : this.dashData.paused
274
275
  ? th.fg("warning", "⏸ PAUSED")
275
276
  : th.fg("dim", "idle");
277
+ const worktreeName = getActiveWorktreeName();
278
+ const worktreeTag = worktreeName
279
+ ? ` ${th.fg("warning", `⎇ ${worktreeName}`)}`
280
+ : "";
276
281
  const elapsed = th.fg("dim", formatDuration(this.dashData.elapsed));
277
- lines.push(row(joinColumns(`${title} ${status}`, elapsed, contentWidth)));
282
+ lines.push(row(joinColumns(`${title} ${status}${worktreeTag}`, elapsed, contentWidth)));
278
283
  lines.push(blank());
279
284
 
280
285
  if (this.dashData.currentUnit) {
@@ -21,7 +21,7 @@ import type {
21
21
  * Split markdown content into frontmatter (YAML-like) and body.
22
22
  * Returns [frontmatterLines, body] where frontmatterLines is null if no frontmatter.
23
23
  */
24
- function splitFrontmatter(content: string): [string[] | null, string] {
24
+ export function splitFrontmatter(content: string): [string[] | null, string] {
25
25
  const trimmed = content.trimStart();
26
26
  if (!trimmed.startsWith('---')) return [null, content];
27
27
 
@@ -42,7 +42,7 @@ function splitFrontmatter(content: string): [string[] | null, string] {
42
42
  * Handles simple scalars and arrays (lines starting with " - ").
43
43
  * Handles nested objects like requires (lines with " key: value").
44
44
  */
45
- function parseFrontmatterMap(lines: string[]): Record<string, unknown> {
45
+ export function parseFrontmatterMap(lines: string[]): Record<string, unknown> {
46
46
  const result: Record<string, unknown> = {};
47
47
  let currentKey: string | null = null;
48
48
  let currentArray: unknown[] | null = null;
@@ -124,7 +124,7 @@ function parseFrontmatterMap(lines: string[]): Record<string, unknown> {
124
124
  }
125
125
 
126
126
  /** Extract the text after a heading at a given level, up to the next heading of same or higher level. */
127
- function extractSection(body: string, heading: string, level: number = 2): string | null {
127
+ export function extractSection(body: string, heading: string, level: number = 2): string | null {
128
128
  const prefix = '#'.repeat(level) + ' ';
129
129
  const regex = new RegExp(`^${prefix}${escapeRegex(heading)}\\s*$`, 'm');
130
130
  const match = regex.exec(body);
@@ -140,7 +140,7 @@ function extractSection(body: string, heading: string, level: number = 2): strin
140
140
  }
141
141
 
142
142
  /** Extract all sections at a given level, returning heading → content map. */
143
- function extractAllSections(body: string, level: number = 2): Map<string, string> {
143
+ export function extractAllSections(body: string, level: number = 2): Map<string, string> {
144
144
  const prefix = '#'.repeat(level) + ' ';
145
145
  const regex = new RegExp(`^${prefix}(.+)$`, 'gm');
146
146
  const sections = new Map<string, string>();
@@ -161,14 +161,14 @@ function escapeRegex(s: string): string {
161
161
  }
162
162
 
163
163
  /** Parse bullet list items from a text block. */
164
- function parseBullets(text: string): string[] {
164
+ export function parseBullets(text: string): string[] {
165
165
  return text.split('\n')
166
166
  .map(l => l.replace(/^\s*[-*]\s+/, '').trim())
167
167
  .filter(l => l.length > 0 && !l.startsWith('#'));
168
168
  }
169
169
 
170
170
  /** Extract key: value from bold-prefixed lines like "**Key:** Value" */
171
- function extractBoldField(text: string, key: string): string | null {
171
+ export function extractBoldField(text: string, key: string): string | null {
172
172
  const regex = new RegExp(`^\\*\\*${escapeRegex(key)}:\\*\\*\\s*(.+)$`, 'm');
173
173
  const match = regex.exec(text);
174
174
  return match ? match[1].trim() : null;
@@ -548,7 +548,7 @@ export function parseRequirementCounts(content: string | null): RequirementCount
548
548
  for (const section of sections) {
549
549
  const text = extractSection(content, section.heading, 2);
550
550
  if (!text) continue;
551
- const matches = text.match(/^###\s+R\d+\s+—/gm);
551
+ const matches = text.match(/^###\s+[A-Z][\w-]*\d+\s+—/gm);
552
552
  counts[section.key] = matches ? matches.length : 0;
553
553
  }
554
554
 
@@ -17,6 +17,7 @@ const BASELINE_PATTERNS = [
17
17
  // ── GSD runtime (not source artifacts) ──
18
18
  ".gsd/activity/",
19
19
  ".gsd/runtime/",
20
+ ".gsd/worktrees/",
20
21
  ".gsd/auto.lock",
21
22
  ".gsd/metrics.json",
22
23
  ".gsd/STATE.md",
@@ -22,8 +22,10 @@ import type {
22
22
  ExtensionAPI,
23
23
  ExtensionContext,
24
24
  } from "@mariozechner/pi-coding-agent";
25
+ import { createBashTool } from "@mariozechner/pi-coding-agent";
25
26
 
26
27
  import { registerGSDCommand } from "./commands.js";
28
+ import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
27
29
  import { saveFile, formatContinue, loadFile, parseContinue, parseSummary } from "./files.js";
28
30
  import { loadPrompt } from "./prompt-loader.js";
29
31
  import { deriveState } from "./state.js";
@@ -59,6 +61,16 @@ const GSD_LOGO_LINES = [
59
61
 
60
62
  export default function (pi: ExtensionAPI) {
61
63
  registerGSDCommand(pi);
64
+ registerWorktreeCommand(pi);
65
+
66
+ // ── Dynamic-cwd bash tool ──────────────────────────────────────────────
67
+ // The built-in bash tool captures cwd at startup. This replacement uses
68
+ // a spawnHook to read process.cwd() dynamically so that process.chdir()
69
+ // (used by /worktree switch) propagates to shell commands.
70
+ const dynamicBash = createBashTool(process.cwd(), {
71
+ spawnHook: (ctx) => ({ ...ctx, cwd: process.cwd() }),
72
+ });
73
+ pi.registerTool(dynamicBash as any);
62
74
 
63
75
  // ── session_start: render branded GSD header ───────────────────────────
64
76
  pi.on("session_start", async (_event, ctx) => {
@@ -131,8 +143,31 @@ export default function (pi: ExtensionAPI) {
131
143
 
132
144
  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
133
145
 
146
+ // Worktree context — override the static CWD in the system prompt
147
+ let worktreeBlock = "";
148
+ const worktreeName = getActiveWorktreeName();
149
+ const worktreeMainCwd = getWorktreeOriginalCwd();
150
+ if (worktreeName && worktreeMainCwd) {
151
+ worktreeBlock = [
152
+ "",
153
+ "",
154
+ "[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
155
+ `IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
156
+ `The actual current working directory is: ${process.cwd()}`,
157
+ "",
158
+ `You are working inside a GSD worktree.`,
159
+ `- Worktree name: ${worktreeName}`,
160
+ `- Worktree path (this is the real cwd): ${process.cwd()}`,
161
+ `- Main project: ${worktreeMainCwd}`,
162
+ `- Branch: worktree/${worktreeName}`,
163
+ "",
164
+ "All file operations, bash commands, and GSD state resolve against the worktree path above.",
165
+ "Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
166
+ ].join("\n");
167
+ }
168
+
134
169
  return {
135
- systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}`,
170
+ systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}${worktreeBlock}`,
136
171
  ...(injection
137
172
  ? {
138
173
  message: {
@@ -0,0 +1,215 @@
1
+ /**
2
+ * /gsd migrate — one-shot migration from .planning to .gsd
3
+ *
4
+ * Thin UX orchestrator: resolves paths, runs the validate → parse → transform →
5
+ * preview → write pipeline, and shows confirmation UI via showNextAction.
6
+ * All business logic lives in the pipeline modules (S01–S03).
7
+ *
8
+ * After a successful write, offers an agent-driven review that audits the
9
+ * output for GSD-2 standards compliance.
10
+ */
11
+
12
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { resolve, join, dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { showNextAction } from "../../shared/next-action-ui.js";
17
+ import {
18
+ validatePlanningDirectory,
19
+ parsePlanningDirectory,
20
+ transformToGSD,
21
+ generatePreview,
22
+ writeGSDDirectory,
23
+ } from "./index.js";
24
+
25
+ import type { MigrationPreview } from "./writer.js";
26
+
27
+ /** Format preview stats for embedding in the review prompt. */
28
+ function formatPreviewStats(preview: MigrationPreview): string {
29
+ const lines = [
30
+ `- Milestones: ${preview.milestoneCount}`,
31
+ `- Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
32
+ `- Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
33
+ ];
34
+ if (preview.requirements.total > 0) {
35
+ lines.push(
36
+ `- Requirements: ${preview.requirements.total} (${preview.requirements.validated} validated, ${preview.requirements.active} active, ${preview.requirements.deferred} deferred)`,
37
+ );
38
+ }
39
+ return lines.join("\n");
40
+ }
41
+
42
+ /** Load and interpolate the review-migration prompt template. */
43
+ function buildReviewPrompt(
44
+ sourcePath: string,
45
+ gsdPath: string,
46
+ preview: MigrationPreview,
47
+ ): string {
48
+ const promptsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "prompts");
49
+ const templatePath = join(promptsDir, "review-migration.md");
50
+ let content = readFileSync(templatePath, "utf-8");
51
+
52
+ content = content.replaceAll("{{sourcePath}}", sourcePath);
53
+ content = content.replaceAll("{{gsdPath}}", gsdPath);
54
+ content = content.replaceAll("{{previewStats}}", formatPreviewStats(preview));
55
+
56
+ return content.trim();
57
+ }
58
+
59
+ /** Dispatch the review prompt to the agent. */
60
+ function dispatchReview(
61
+ pi: ExtensionAPI,
62
+ sourcePath: string,
63
+ gsdPath: string,
64
+ preview: MigrationPreview,
65
+ ): void {
66
+ const prompt = buildReviewPrompt(sourcePath, gsdPath, preview);
67
+
68
+ pi.sendMessage(
69
+ {
70
+ customType: "gsd-migrate-review",
71
+ content: prompt,
72
+ display: false,
73
+ },
74
+ { triggerTurn: true },
75
+ );
76
+ }
77
+
78
+ export async function handleMigrate(
79
+ args: string,
80
+ ctx: ExtensionCommandContext,
81
+ pi: ExtensionAPI,
82
+ ): Promise<void> {
83
+ // ── Resolve source path ────────────────────────────────────────────────────
84
+ // Default to cwd when no args given; expand ~ to HOME
85
+ let rawPath = args.trim() || ".";
86
+ if (rawPath.startsWith("~/")) {
87
+ rawPath = join(process.env.HOME ?? "~", rawPath.slice(2));
88
+ } else if (rawPath === "~") {
89
+ rawPath = process.env.HOME ?? "~";
90
+ }
91
+
92
+ let sourcePath = resolve(process.cwd(), rawPath);
93
+ if (!sourcePath.endsWith(".planning")) {
94
+ sourcePath = join(sourcePath, ".planning");
95
+ }
96
+
97
+ if (!existsSync(sourcePath)) {
98
+ ctx.ui.notify(
99
+ `Directory not found: ${sourcePath}\n\nMake sure the path points to a project root with a .planning directory.`,
100
+ "error",
101
+ );
102
+ return;
103
+ }
104
+
105
+ // ── Validate ───────────────────────────────────────────────────────────────
106
+ const validation = await validatePlanningDirectory(sourcePath);
107
+
108
+ const warnings = validation.issues.filter((i) => i.severity === "warning");
109
+ const fatals = validation.issues.filter((i) => i.severity === "fatal");
110
+
111
+ for (const w of warnings) {
112
+ ctx.ui.notify(`⚠ ${w.message} (${w.file})`, "warning");
113
+ }
114
+ for (const f of fatals) {
115
+ ctx.ui.notify(`✖ ${f.message} (${f.file})`, "error");
116
+ }
117
+
118
+ if (!validation.valid) {
119
+ ctx.ui.notify(
120
+ "Migration blocked — fix the fatal issues above before retrying.",
121
+ "error",
122
+ );
123
+ return;
124
+ }
125
+
126
+ // ── Parse → Transform → Preview ───────────────────────────────────────────
127
+ const parsed = await parsePlanningDirectory(sourcePath);
128
+ const project = transformToGSD(parsed);
129
+ const preview = generatePreview(project);
130
+
131
+ // ── Build preview text ─────────────────────────────────────────────────────
132
+ const lines: string[] = [
133
+ `Milestones: ${preview.milestoneCount}`,
134
+ `Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
135
+ `Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
136
+ ];
137
+
138
+ if (preview.requirements.total > 0) {
139
+ lines.push(
140
+ `Requirements: ${preview.requirements.total} (${preview.requirements.validated} validated, ${preview.requirements.active} active, ${preview.requirements.deferred} deferred)`,
141
+ );
142
+ }
143
+
144
+ const targetGsdExists = existsSync(join(process.cwd(), ".gsd"));
145
+ if (targetGsdExists) {
146
+ lines.push("");
147
+ lines.push("⚠ A .gsd directory already exists in the current working directory — it will be overwritten.");
148
+ }
149
+
150
+ // ── Confirmation via showNextAction ────────────────────────────────────────
151
+ const choice = await showNextAction(ctx as any, {
152
+ title: "Migration preview",
153
+ summary: lines,
154
+ actions: [
155
+ {
156
+ id: "confirm",
157
+ label: "Write .gsd directory",
158
+ description: `Migrate ${preview.milestoneCount} milestone(s) to ${process.cwd()}/.gsd`,
159
+ recommended: true,
160
+ },
161
+ {
162
+ id: "cancel",
163
+ label: "Cancel",
164
+ description: "Exit without writing anything",
165
+ },
166
+ ],
167
+ notYetMessage: "Run /gsd migrate again when ready.",
168
+ });
169
+
170
+ if (choice !== "confirm") {
171
+ ctx.ui.notify("Migration cancelled — no files were written.", "info");
172
+ return;
173
+ }
174
+
175
+ // ── Write ──────────────────────────────────────────────────────────────────
176
+ ctx.ui.notify("Writing .gsd directory…", "info");
177
+
178
+ const result = await writeGSDDirectory(project, process.cwd());
179
+ const gsdPath = join(process.cwd(), ".gsd");
180
+
181
+ ctx.ui.notify(
182
+ `✓ Migration complete — ${result.paths.length} file(s) written to .gsd/`,
183
+ "info",
184
+ );
185
+
186
+ // ── Post-write review offer ────────────────────────────────────────────────
187
+ const reviewChoice = await showNextAction(ctx as any, {
188
+ title: "Migration written",
189
+ summary: [
190
+ `${result.paths.length} files written to .gsd/`,
191
+ "",
192
+ "The agent can now review the migrated output against GSD-2 standards —",
193
+ "checking structure, content quality, deriveState() round-trip, and",
194
+ "requirement statuses. It will fix minor issues in-place.",
195
+ ],
196
+ actions: [
197
+ {
198
+ id: "review",
199
+ label: "Review migration",
200
+ description: "Agent audits the .gsd output and reports PASS/FAIL per category",
201
+ recommended: true,
202
+ },
203
+ {
204
+ id: "skip",
205
+ label: "Skip review",
206
+ description: "Trust the migration output as-is",
207
+ },
208
+ ],
209
+ notYetMessage: "Run /gsd migrate again to re-migrate, or review .gsd manually.",
210
+ });
211
+
212
+ if (reviewChoice === "review") {
213
+ dispatchReview(pi, sourcePath, gsdPath, preview);
214
+ }
215
+ }
@@ -0,0 +1,42 @@
1
+ // Barrel export for old .planning migration module
2
+
3
+ export { handleMigrate } from './command.ts';
4
+ export { parsePlanningDirectory } from './parser.ts';
5
+ export { validatePlanningDirectory } from './validator.ts';
6
+ export { transformToGSD } from './transformer.ts';
7
+ export { writeGSDDirectory } from './writer.ts';
8
+ export type { WrittenFiles, MigrationPreview } from './writer.ts';
9
+ export { generatePreview } from './preview.ts';
10
+ export type {
11
+ // Input types (old .planning format)
12
+ PlanningProject,
13
+ PlanningPhase,
14
+ PlanningPlan,
15
+ PlanningPlanFrontmatter,
16
+ PlanningPlanMustHaves,
17
+ PlanningSummary,
18
+ PlanningSummaryFrontmatter,
19
+ PlanningSummaryRequires,
20
+ PlanningRoadmap,
21
+ PlanningRoadmapMilestone,
22
+ PlanningRoadmapEntry,
23
+ PlanningRequirement,
24
+ PlanningResearch,
25
+ PlanningConfig,
26
+ PlanningQuickTask,
27
+ PlanningMilestone,
28
+ PlanningState,
29
+ PlanningPhaseFile,
30
+ ValidationResult,
31
+ ValidationIssue,
32
+ ValidationSeverity,
33
+ // Output types (GSD-2 format)
34
+ GSDProject,
35
+ GSDMilestone,
36
+ GSDSlice,
37
+ GSDTask,
38
+ GSDRequirement,
39
+ GSDSliceSummaryData,
40
+ GSDTaskSummaryData,
41
+ GSDBoundaryEntry,
42
+ } from './types.ts';