code-as-plan 2.0.2 → 2.0.4

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.
package/bin/install.js CHANGED
@@ -61,7 +61,7 @@ const hasCopilot = args.includes('--copilot');
61
61
  const hasAntigravity = args.includes('--antigravity');
62
62
  const hasCursor = args.includes('--cursor');
63
63
  const hasWindsurf = args.includes('--windsurf');
64
- const hasSdk = args.includes('--sdk');
64
+ // SDK install removed in CAP v2.0 — not needed
65
65
  const hasBoth = args.includes('--both'); // Legacy flag, keeps working
66
66
  const hasAll = args.includes('--all');
67
67
  const hasUninstall = args.includes('--uninstall') || args.includes('-u');
@@ -322,7 +322,7 @@ if (hasUninstall) {
322
322
 
323
323
  // Show help if requested
324
324
  if (hasHelp) {
325
- console.log(` ${yellow}Usage:${reset} npx code-as-plan [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}--sdk${reset} Also install CAP SDK CLI (cap-sdk)\n ${cyan}-u, --uninstall${reset} Uninstall CAP (remove all CAP files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx code-as-plan\n\n ${dim}# Install for Claude Code globally${reset}\n npx code-as-plan --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx code-as-plan --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx code-as-plan --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx code-as-plan --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx code-as-plan --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx code-as-plan --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx code-as-plan --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx code-as-plan --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx code-as-plan --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx code-as-plan --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx code-as-plan --windsurf --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx code-as-plan --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx code-as-plan --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx code-as-plan --claude --local\n\n ${dim}# Uninstall CAP from Cursor globally${reset}\n npx code-as-plan --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR environment variables.\n`);
325
+ console.log(` ${yellow}Usage:${reset} npx code-as-plan [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall CAP (remove all CAP files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx code-as-plan\n\n ${dim}# Install for Claude Code globally${reset}\n npx code-as-plan --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx code-as-plan --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx code-as-plan --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx code-as-plan --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx code-as-plan --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx code-as-plan --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx code-as-plan --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx code-as-plan --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx code-as-plan --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx code-as-plan --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx code-as-plan --windsurf --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx code-as-plan --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx code-as-plan --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx code-as-plan --claude --local\n\n ${dim}# Uninstall CAP from Cursor globally${reset}\n npx code-as-plan --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR environment variables.\n`);
326
326
  process.exit(0);
327
327
  }
328
328
 
@@ -4691,70 +4691,6 @@ function handleStatusline(settings, isInteractive, callback) {
4691
4691
  });
4692
4692
  }
4693
4693
 
4694
- /**
4695
- * Install the CAP SDK globally via npm.
4696
- * @returns {boolean} true if install succeeded
4697
- */
4698
- function installSdk() {
4699
- const sdkPkg = `@gsd-build/sdk@latest`;
4700
- console.log(`\n ${cyan}Installing CAP SDK...${reset}`);
4701
- console.log(` ${dim}npm install -g ${sdkPkg}${reset}\n`);
4702
- try {
4703
- require('child_process').execSync(`npm install -g ${sdkPkg}`, { stdio: 'inherit' });
4704
- console.log(`\n ${green}✓${reset} CAP SDK installed (${cyan}cap-sdk${reset} command available)`);
4705
- return true;
4706
- } catch (e) {
4707
- console.log(`\n ${yellow}⚠${reset} SDK install failed: ${e.message}`);
4708
- console.log(` ${dim}You can install it manually: npm install -g ${sdkPkg}${reset}`);
4709
- return false;
4710
- }
4711
- }
4712
-
4713
- /**
4714
- * Prompt the user to optionally install the CAP SDK.
4715
- * Called after runtime installation completes.
4716
- * @param {Function} callback - called with true/false
4717
- */
4718
- function promptSdk(callback) {
4719
- if (!process.stdin.isTTY) {
4720
- callback(false);
4721
- return;
4722
- }
4723
-
4724
- const rl = readline.createInterface({
4725
- input: process.stdin,
4726
- output: process.stdout
4727
- });
4728
-
4729
- let answered = false;
4730
-
4731
- rl.on('close', () => {
4732
- if (!answered) {
4733
- answered = true;
4734
- callback(false);
4735
- }
4736
- });
4737
-
4738
- console.log(`
4739
- ${yellow}Also install the CAP SDK?${reset}
4740
-
4741
- The SDK provides a standalone CLI for autonomous execution:
4742
- ${dim}cap-sdk init @prd.md${reset} Bootstrap a project from a PRD
4743
- ${dim}cap-sdk auto${reset} Run full autonomous lifecycle
4744
- ${dim}cap-sdk run "prompt"${reset} Execute a milestone from text
4745
-
4746
- ${cyan}1${reset}) No
4747
- ${cyan}2${reset}) Yes ${dim}(runs: npm install -g @gsd-build/sdk)${reset}
4748
- `);
4749
-
4750
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
4751
- answered = true;
4752
- rl.close();
4753
- const choice = answer.trim() || '1';
4754
- callback(choice === '2');
4755
- });
4756
- }
4757
-
4758
4694
  /**
4759
4695
  * Prompt for runtime selection
4760
4696
  */
@@ -4884,7 +4820,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
4884
4820
  const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
4885
4821
 
4886
4822
  const finalize = (shouldInstallStatusline) => {
4887
- // Handle SDK installation before printing final summaries
4823
+ // Print final summaries
4888
4824
  const printSummaries = () => {
4889
4825
  for (const result of results) {
4890
4826
  const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
@@ -4899,18 +4835,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
4899
4835
  }
4900
4836
  };
4901
4837
 
4902
- if (hasSdk) {
4903
- // --sdk flag: install without prompting
4904
- installSdk();
4905
- printSummaries();
4906
- } else if (isInteractive) {
4907
- promptSdk((wantsSdk) => {
4908
- if (wantsSdk) installSdk();
4909
- printSummaries();
4910
- });
4911
- } else {
4912
- printSummaries();
4913
- }
4838
+ printSummaries();
4914
4839
  };
4915
4840
 
4916
4841
  if (primaryStatuslineResult) {
@@ -0,0 +1,534 @@
1
+ // @cap-feature(feature:F-MIGRATE) GSD-to-CAP migration utility -- converts @gsd-* tags, planning artifacts, and session format to CAP v2.0.
2
+ // @cap-todo decision: Regex-based tag replacement (not AST) -- language-agnostic, zero dependencies, handles all comment styles.
3
+ // @cap-todo risk: Destructive file writes -- dry-run mode is the default safety net.
4
+
5
+ 'use strict';
6
+
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+
10
+ // --- Constants ---
11
+
12
+ const GSD_TAG_RE = /(@gsd-(feature|todos?|risk|decision|context|status|depends|ref|pattern|api|constraint|placeholder|concern))(\([^)]*\))?\s*(.*)/;
13
+
14
+ const SUPPORTED_EXTENSIONS = ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.rb', '.go', '.rs', '.sh', '.md'];
15
+ const EXCLUDE_DIRS = ['node_modules', '.git', '.cap', 'dist', 'build', 'coverage'];
16
+
17
+ const GSD_ARTIFACTS = [
18
+ '.planning/FEATURES.md',
19
+ '.planning/REQUIREMENTS.md',
20
+ '.planning/PRD.md',
21
+ '.planning/ROADMAP.md',
22
+ '.planning/STATE.md',
23
+ '.planning/CODE-INVENTORY.md',
24
+ '.planning/BRAINSTORM-LEDGER.md',
25
+ '.planning/SESSION.json',
26
+ ];
27
+
28
+ // --- Tag migration ---
29
+
30
+ /**
31
+ * @typedef {Object} TagChange
32
+ * @property {string} file - Relative file path
33
+ * @property {number} line - 1-based line number
34
+ * @property {string} original - Original line content
35
+ * @property {string} replaced - Replacement line content (or null if removed)
36
+ * @property {string} action - 'converted' | 'removed' | 'plain-comment'
37
+ */
38
+
39
+ /**
40
+ * Apply tag migration to a single line.
41
+ * @param {string} line - Source line
42
+ * @returns {{ replaced: string, action: string } | null} - null if no @gsd- tag found
43
+ */
44
+ function migrateLineTag(line) {
45
+ const match = line.match(GSD_TAG_RE);
46
+ if (!match) return null;
47
+
48
+ const fullTag = match[1]; // e.g., @gsd-feature
49
+ const tagType = match[2]; // e.g., feature
50
+ const metadata = match[3] || ''; // e.g., (ref:AC-20)
51
+ const description = match[4] || '';
52
+
53
+ switch (tagType) {
54
+ case 'feature':
55
+ return {
56
+ replaced: line.replace(fullTag, '@cap-feature'),
57
+ action: 'converted',
58
+ };
59
+
60
+ case 'todo':
61
+ return {
62
+ replaced: line.replace(fullTag, '@cap-todo'),
63
+ action: 'converted',
64
+ };
65
+
66
+ case 'risk':
67
+ // @gsd-risk Some risk → @cap-todo risk: Some risk
68
+ return {
69
+ replaced: line.replace(fullTag + metadata + (description ? ' ' : ''), '@cap-todo' + metadata + ' risk: ').replace(/ +/g, ' '),
70
+ action: 'converted',
71
+ };
72
+
73
+ case 'decision':
74
+ // @gsd-decision Some decision → @cap-todo decision: Some decision
75
+ return {
76
+ replaced: line.replace(fullTag + metadata + (description ? ' ' : ''), '@cap-todo' + metadata + ' decision: ').replace(/ +/g, ' '),
77
+ action: 'converted',
78
+ };
79
+
80
+ case 'constraint':
81
+ // @gsd-constraint Some constraint → @cap-todo risk: [constraint] Some constraint
82
+ return {
83
+ replaced: line.replace(fullTag + metadata + (description ? ' ' : ''), '@cap-todo' + metadata + ' risk: [constraint] ').replace(/ +/g, ' '),
84
+ action: 'converted',
85
+ };
86
+
87
+ case 'context':
88
+ // @gsd-context Some context → plain comment (remove the tag)
89
+ return {
90
+ replaced: line.replace(fullTag + metadata + ' ', '').replace(fullTag + metadata, ''),
91
+ action: 'plain-comment',
92
+ };
93
+
94
+ case 'status':
95
+ case 'depends':
96
+ // Remove entirely (convert to plain comment to avoid losing info)
97
+ return {
98
+ replaced: line.replace(fullTag + metadata + ' ', '').replace(fullTag + metadata, ''),
99
+ action: 'removed',
100
+ };
101
+
102
+ case 'ref':
103
+ // Keep as @cap-ref if it has content, otherwise remove
104
+ if (description.trim()) {
105
+ return {
106
+ replaced: line.replace(fullTag, '@cap-ref'),
107
+ action: 'converted',
108
+ };
109
+ }
110
+ return {
111
+ replaced: line.replace(fullTag + metadata + ' ', '').replace(fullTag + metadata, ''),
112
+ action: 'removed',
113
+ };
114
+
115
+ case 'pattern':
116
+ case 'api':
117
+ // Convert to plain comment (remove the tag prefix)
118
+ return {
119
+ replaced: line.replace(fullTag + metadata + ' ', '').replace(fullTag + metadata, ''),
120
+ action: 'plain-comment',
121
+ };
122
+
123
+ case 'todos':
124
+ // @gsd-todos (plural typo) → @cap-todo
125
+ return {
126
+ replaced: line.replace(fullTag, '@cap-todo'),
127
+ action: 'converted',
128
+ };
129
+
130
+ case 'placeholder':
131
+ // @gsd-placeholder → @cap-todo (placeholder is a todo variant)
132
+ return {
133
+ replaced: line.replace(fullTag, '@cap-todo'),
134
+ action: 'converted',
135
+ };
136
+
137
+ case 'concern':
138
+ // @gsd-concern → @cap-todo risk: (concerns are risks)
139
+ return {
140
+ replaced: line.replace(fullTag + metadata + (description ? ' ' : ''), '@cap-todo' + metadata + ' risk: ').replace(/ +/g, ' '),
141
+ action: 'converted',
142
+ };
143
+
144
+ default:
145
+ return null;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Scan all source files and replace @gsd-* tags with @cap-* equivalents.
151
+ *
152
+ * Mapping:
153
+ * @gsd-feature → @cap-feature
154
+ * @gsd-todo → @cap-todo
155
+ * @gsd-risk → @cap-todo risk:
156
+ * @gsd-decision → @cap-todo decision:
157
+ * @gsd-context → plain comment (tag removed)
158
+ * @gsd-status → plain comment (tag removed)
159
+ * @gsd-depends → plain comment (tag removed)
160
+ * @gsd-ref → @cap-ref (if content exists) or removed
161
+ * @gsd-pattern → plain comment (tag removed)
162
+ * @gsd-api → plain comment (tag removed)
163
+ * @gsd-constraint → @cap-todo risk: [constraint]
164
+ *
165
+ * @param {string} projectRoot - Absolute path to project root
166
+ * @param {Object} [options]
167
+ * @param {boolean} [options.dryRun] - If true, report changes without writing
168
+ * @param {string[]} [options.extensions] - File extensions to process
169
+ * @returns {{ filesScanned: number, filesModified: number, tagsConverted: number, tagsRemoved: number, changes: TagChange[] }}
170
+ */
171
+ function migrateTags(projectRoot, options = {}) {
172
+ const dryRun = options.dryRun || false;
173
+ const extensions = options.extensions || SUPPORTED_EXTENSIONS;
174
+ const result = {
175
+ filesScanned: 0,
176
+ filesModified: 0,
177
+ tagsConverted: 0,
178
+ tagsRemoved: 0,
179
+ changes: [],
180
+ };
181
+
182
+ function walk(dir) {
183
+ let entries;
184
+ try {
185
+ entries = fs.readdirSync(dir, { withFileTypes: true });
186
+ } catch (_e) {
187
+ return;
188
+ }
189
+ for (const entry of entries) {
190
+ const fullPath = path.join(dir, entry.name);
191
+ if (entry.isDirectory()) {
192
+ if (EXCLUDE_DIRS.includes(entry.name)) continue;
193
+ walk(fullPath);
194
+ } else if (entry.isFile()) {
195
+ const ext = path.extname(entry.name);
196
+ if (!extensions.includes(ext)) continue;
197
+ processFile(fullPath);
198
+ }
199
+ }
200
+ }
201
+
202
+ function processFile(filePath) {
203
+ let content;
204
+ try {
205
+ content = fs.readFileSync(filePath, 'utf8');
206
+ } catch (_e) {
207
+ return;
208
+ }
209
+
210
+ result.filesScanned++;
211
+ const relativePath = path.relative(projectRoot, filePath);
212
+ const lines = content.split('\n');
213
+ let modified = false;
214
+
215
+ for (let i = 0; i < lines.length; i++) {
216
+ const migration = migrateLineTag(lines[i]);
217
+ if (!migration) continue;
218
+
219
+ const change = {
220
+ file: relativePath,
221
+ line: i + 1,
222
+ original: lines[i],
223
+ replaced: migration.replaced,
224
+ action: migration.action,
225
+ };
226
+ result.changes.push(change);
227
+
228
+ if (migration.action === 'converted') {
229
+ result.tagsConverted++;
230
+ } else {
231
+ result.tagsRemoved++;
232
+ }
233
+
234
+ lines[i] = migration.replaced;
235
+ modified = true;
236
+ }
237
+
238
+ if (modified) {
239
+ result.filesModified++;
240
+ if (!dryRun) {
241
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
242
+ }
243
+ }
244
+ }
245
+
246
+ walk(projectRoot);
247
+ return result;
248
+ }
249
+
250
+ // --- Artifact migration ---
251
+
252
+ /**
253
+ * Convert .planning/FEATURES.md or REQUIREMENTS.md into FEATURE-MAP.md format.
254
+ *
255
+ * @param {string} projectRoot - Absolute path to project root
256
+ * @param {Object} [options]
257
+ * @param {boolean} [options.dryRun] - If true, report without writing
258
+ * @returns {{ featuresFound: number, featureMapCreated: boolean, source: string }}
259
+ */
260
+ function migrateArtifacts(projectRoot, options = {}) {
261
+ const dryRun = options.dryRun || false;
262
+ const result = { featuresFound: 0, featureMapCreated: false, source: 'none' };
263
+
264
+ // Check if FEATURE-MAP.md already exists
265
+ const featureMapPath = path.join(projectRoot, 'FEATURE-MAP.md');
266
+ const featureMapExists = fs.existsSync(featureMapPath);
267
+
268
+ // Try reading source artifacts in priority order
269
+ let sourceContent = null;
270
+ let sourceName = null;
271
+
272
+ const sources = [
273
+ { file: '.planning/FEATURES.md', name: 'FEATURES.md' },
274
+ { file: '.planning/REQUIREMENTS.md', name: 'REQUIREMENTS.md' },
275
+ { file: '.planning/PRD.md', name: 'PRD.md' },
276
+ ];
277
+
278
+ for (const src of sources) {
279
+ const srcPath = path.join(projectRoot, src.file);
280
+ if (fs.existsSync(srcPath)) {
281
+ try {
282
+ sourceContent = fs.readFileSync(srcPath, 'utf8');
283
+ sourceName = src.name;
284
+ result.source = src.name;
285
+ break;
286
+ } catch (_e) {
287
+ continue;
288
+ }
289
+ }
290
+ }
291
+
292
+ if (!sourceContent) return result;
293
+
294
+ // Extract features from the source artifact
295
+ const features = extractFeaturesFromLegacy(sourceContent);
296
+ result.featuresFound = features.length;
297
+
298
+ if (features.length === 0) return result;
299
+
300
+ if (featureMapExists) {
301
+ // Merge into existing Feature Map
302
+ const capFeatureMap = require('./cap-feature-map.cjs');
303
+ if (!dryRun) {
304
+ const existing = capFeatureMap.readFeatureMap(projectRoot);
305
+ const existingTitles = new Set(existing.features.map(f => f.title.toLowerCase()));
306
+
307
+ for (const feature of features) {
308
+ if (!existingTitles.has(feature.title.toLowerCase())) {
309
+ capFeatureMap.addFeature(projectRoot, feature);
310
+ }
311
+ }
312
+ result.featureMapCreated = true;
313
+ }
314
+ } else {
315
+ // Create new Feature Map
316
+ if (!dryRun) {
317
+ const capFeatureMap = require('./cap-feature-map.cjs');
318
+ const template = capFeatureMap.generateTemplate();
319
+ fs.writeFileSync(featureMapPath, template, 'utf8');
320
+ for (const feature of features) {
321
+ capFeatureMap.addFeature(projectRoot, feature);
322
+ }
323
+ result.featureMapCreated = true;
324
+ } else {
325
+ result.featureMapCreated = true; // Would be created
326
+ }
327
+ }
328
+
329
+ return result;
330
+ }
331
+
332
+ /**
333
+ * Extract feature entries from legacy GSD planning artifacts.
334
+ * Looks for markdown headings, list items with feature-like patterns.
335
+ *
336
+ * @param {string} content - Markdown content of legacy artifact
337
+ * @returns {{ title: string, acs: Array, dependencies: string[] }[]}
338
+ */
339
+ function extractFeaturesFromLegacy(content) {
340
+ const features = [];
341
+ const lines = content.split('\n');
342
+
343
+ // Match headings that look like features: ## Feature Name, ### Feature Name, ## 1. Feature Name
344
+ const featureHeadingRE = /^#{2,4}\s+(?:\d+\.\s*)?(?:Feature:\s*)?(.+?)(?:\s*\[.*\])?\s*$/;
345
+ // Match list items that look like acceptance criteria: - [ ] description, - [x] description
346
+ const acRE = /^[-*]\s+\[([x ])\]\s+(.+)/i;
347
+ // Match plain list items as potential ACs
348
+ const plainListRE = /^[-*]\s+(?!#)(.+)/;
349
+
350
+ let currentFeature = null;
351
+ let acCounter = 0;
352
+
353
+ for (const line of lines) {
354
+ const headingMatch = line.match(featureHeadingRE);
355
+ if (headingMatch) {
356
+ if (currentFeature && currentFeature.title) {
357
+ features.push(currentFeature);
358
+ }
359
+ currentFeature = {
360
+ title: headingMatch[1].trim(),
361
+ acs: [],
362
+ dependencies: [],
363
+ };
364
+ acCounter = 0;
365
+ continue;
366
+ }
367
+
368
+ if (currentFeature) {
369
+ const acMatch = line.match(acRE);
370
+ if (acMatch) {
371
+ acCounter++;
372
+ currentFeature.acs.push({
373
+ id: `AC-${acCounter}`,
374
+ description: acMatch[2].trim(),
375
+ status: acMatch[1] === 'x' || acMatch[1] === 'X' ? 'implemented' : 'pending',
376
+ });
377
+ continue;
378
+ }
379
+
380
+ // Empty line after ACs but before next heading -- stop collecting ACs
381
+ if (line.trim() === '' && currentFeature.acs.length > 0) {
382
+ // Keep collecting -- next heading or feature resets
383
+ }
384
+ }
385
+ }
386
+
387
+ if (currentFeature && currentFeature.title) {
388
+ features.push(currentFeature);
389
+ }
390
+
391
+ return features;
392
+ }
393
+
394
+ // --- Session migration ---
395
+
396
+ /**
397
+ * Migrate .planning/SESSION.json to .cap/SESSION.json format.
398
+ *
399
+ * @param {string} projectRoot - Absolute path to project root
400
+ * @param {Object} [options]
401
+ * @param {boolean} [options.dryRun] - If true, report without writing
402
+ * @returns {{ migrated: boolean, oldFormat: string, newFormat: string }}
403
+ */
404
+ function migrateSession(projectRoot, options = {}) {
405
+ const dryRun = options.dryRun || false;
406
+ const result = { migrated: false, oldFormat: 'none', newFormat: 'none' };
407
+
408
+ const oldSessionPath = path.join(projectRoot, '.planning', 'SESSION.json');
409
+ if (!fs.existsSync(oldSessionPath)) return result;
410
+
411
+ let oldSession;
412
+ try {
413
+ const content = fs.readFileSync(oldSessionPath, 'utf8');
414
+ oldSession = JSON.parse(content);
415
+ result.oldFormat = 'v1.x';
416
+ } catch (_e) {
417
+ result.oldFormat = 'corrupt';
418
+ return result;
419
+ }
420
+
421
+ // Map old session fields to new CAP session format
422
+ const capSession = require('./cap-session.cjs');
423
+ const newSession = capSession.getDefaultSession();
424
+
425
+ // Map known v1.x fields
426
+ if (oldSession.current_app) {
427
+ newSession.metadata.legacyApp = oldSession.current_app;
428
+ }
429
+ if (oldSession.current_phase) {
430
+ newSession.step = `legacy-phase-${oldSession.current_phase}`;
431
+ }
432
+ if (oldSession.started_at || oldSession.startedAt) {
433
+ newSession.startedAt = oldSession.started_at || oldSession.startedAt;
434
+ }
435
+ if (oldSession.last_command || oldSession.lastCommand) {
436
+ newSession.lastCommand = oldSession.last_command || oldSession.lastCommand;
437
+ }
438
+
439
+ // Preserve all old fields as metadata for reference
440
+ for (const [key, value] of Object.entries(oldSession)) {
441
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
442
+ newSession.metadata[`gsd_${key}`] = String(value);
443
+ }
444
+ }
445
+
446
+ result.newFormat = 'v2.0';
447
+
448
+ if (!dryRun) {
449
+ capSession.initCapDirectory(projectRoot);
450
+ capSession.saveSession(projectRoot, newSession);
451
+ result.migrated = true;
452
+ } else {
453
+ result.migrated = true; // Would be migrated
454
+ }
455
+
456
+ return result;
457
+ }
458
+
459
+ // --- Analysis ---
460
+
461
+ /**
462
+ * Generate a migration report summarizing what was found and what needs attention.
463
+ *
464
+ * @param {string} projectRoot - Absolute path to project root
465
+ * @returns {{ gsdTagCount: number, gsdArtifacts: string[], planningDir: boolean, sessionJson: boolean, recommendations: string[] }}
466
+ */
467
+ function analyzeMigration(projectRoot) {
468
+ const result = {
469
+ gsdTagCount: 0,
470
+ gsdArtifacts: [],
471
+ planningDir: false,
472
+ sessionJson: false,
473
+ recommendations: [],
474
+ };
475
+
476
+ // Check for .planning/ directory
477
+ const planningDir = path.join(projectRoot, '.planning');
478
+ result.planningDir = fs.existsSync(planningDir);
479
+
480
+ // Check for known GSD artifacts
481
+ for (const artifact of GSD_ARTIFACTS) {
482
+ const artifactPath = path.join(projectRoot, artifact);
483
+ if (fs.existsSync(artifactPath)) {
484
+ result.gsdArtifacts.push(artifact);
485
+ }
486
+ }
487
+
488
+ // Check for .planning/SESSION.json specifically
489
+ result.sessionJson = fs.existsSync(path.join(projectRoot, '.planning', 'SESSION.json'));
490
+
491
+ // Count @gsd-* tags in source files
492
+ const tagResult = migrateTags(projectRoot, { dryRun: true });
493
+ result.gsdTagCount = tagResult.tagsConverted + tagResult.tagsRemoved;
494
+
495
+ // Build recommendations
496
+ if (result.gsdTagCount > 0) {
497
+ result.recommendations.push(`Found ${result.gsdTagCount} @gsd-* tags to migrate. Run /cap:migrate to convert them to @cap-* tags.`);
498
+ }
499
+
500
+ if (result.gsdArtifacts.length > 0) {
501
+ result.recommendations.push(`Found ${result.gsdArtifacts.length} legacy planning artifacts: ${result.gsdArtifacts.join(', ')}. These can be converted to FEATURE-MAP.md entries.`);
502
+ }
503
+
504
+ if (result.sessionJson) {
505
+ result.recommendations.push('Found .planning/SESSION.json. This can be migrated to .cap/SESSION.json format.');
506
+ }
507
+
508
+ if (!fs.existsSync(path.join(projectRoot, 'FEATURE-MAP.md'))) {
509
+ result.recommendations.push('No FEATURE-MAP.md found. Migration will create one from existing artifacts.');
510
+ }
511
+
512
+ if (!fs.existsSync(path.join(projectRoot, '.cap'))) {
513
+ result.recommendations.push('No .cap/ directory found. Migration will initialize it.');
514
+ }
515
+
516
+ if (result.gsdTagCount === 0 && result.gsdArtifacts.length === 0 && !result.sessionJson) {
517
+ result.recommendations.push('No GSD v1.x artifacts detected. This project may already be using CAP v2.0 or is a fresh project.');
518
+ }
519
+
520
+ return result;
521
+ }
522
+
523
+ module.exports = {
524
+ GSD_TAG_RE,
525
+ SUPPORTED_EXTENSIONS,
526
+ EXCLUDE_DIRS,
527
+ GSD_ARTIFACTS,
528
+ migrateLineTag,
529
+ migrateTags,
530
+ migrateArtifacts,
531
+ extractFeaturesFromLegacy,
532
+ migrateSession,
533
+ analyzeMigration,
534
+ };
@@ -439,11 +439,82 @@ function groupByPackage(tags, packages) {
439
439
  return groups;
440
440
  }
441
441
 
442
+ // @cap-todo Detect legacy @gsd-* tags and recommend /cap:migrate
443
+ const LEGACY_TAG_RE = /^[ \t]*(?:\/\/|\/\*|\*|#|--|"""|''')[ \t]*@gsd-(feature|todo|risk|decision|context|status|depends|ref|pattern|api|constraint)/;
444
+
445
+ /**
446
+ * Detect legacy @gsd-* tags in scanned files.
447
+ * Re-scans source files for @gsd-* patterns that the primary scanner ignores.
448
+ *
449
+ * @param {string} projectRoot - Absolute path to project root
450
+ * @param {Object} [options]
451
+ * @param {string[]} [options.extensions] - File extensions to include
452
+ * @param {string[]} [options.exclude] - Directory names to exclude
453
+ * @returns {{ count: number, files: string[], recommendation: string }}
454
+ */
455
+ function detectLegacyTags(projectRoot, options = {}) {
456
+ const extensions = options.extensions || SUPPORTED_EXTENSIONS;
457
+ const exclude = options.exclude || DEFAULT_EXCLUDE;
458
+ const result = { count: 0, files: [], recommendation: '' };
459
+ const fileSet = new Set();
460
+
461
+ function walk(dir) {
462
+ let entries;
463
+ try {
464
+ entries = fs.readdirSync(dir, { withFileTypes: true });
465
+ } catch (_e) {
466
+ return;
467
+ }
468
+ for (const entry of entries) {
469
+ const fullPath = path.join(dir, entry.name);
470
+ if (entry.isDirectory()) {
471
+ if (exclude.includes(entry.name)) continue;
472
+ walk(fullPath);
473
+ } else if (entry.isFile()) {
474
+ const ext = path.extname(entry.name);
475
+ if (!extensions.includes(ext)) continue;
476
+ scanFileForLegacy(fullPath);
477
+ }
478
+ }
479
+ }
480
+
481
+ function scanFileForLegacy(filePath) {
482
+ let content;
483
+ try {
484
+ content = fs.readFileSync(filePath, 'utf8');
485
+ } catch (_e) {
486
+ return;
487
+ }
488
+ const lines = content.split('\n');
489
+ let found = false;
490
+ for (const line of lines) {
491
+ if (LEGACY_TAG_RE.test(line)) {
492
+ result.count++;
493
+ found = true;
494
+ }
495
+ }
496
+ if (found) {
497
+ const relativePath = path.relative(projectRoot, filePath);
498
+ fileSet.add(relativePath);
499
+ }
500
+ }
501
+
502
+ walk(projectRoot);
503
+ result.files = Array.from(fileSet).sort();
504
+
505
+ if (result.count > 0) {
506
+ result.recommendation = `Found ${result.count} legacy @gsd-* tag(s) in ${result.files.length} file(s). Run /cap:migrate to convert them to @cap-* format.`;
507
+ }
508
+
509
+ return result;
510
+ }
511
+
442
512
  module.exports = {
443
513
  CAP_TAG_TYPES,
444
514
  CAP_TAG_RE,
445
515
  SUPPORTED_EXTENSIONS,
446
516
  DEFAULT_EXCLUDE,
517
+ LEGACY_TAG_RE,
447
518
  scanFile,
448
519
  scanDirectory,
449
520
  extractTags,
@@ -455,4 +526,5 @@ module.exports = {
455
526
  resolveWorkspaceGlobs,
456
527
  scanMonorepo,
457
528
  groupByPackage,
529
+ detectLegacyTags,
458
530
  };
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: cap:migrate
3
+ description: "Migrate GSD Code-First v1.x projects to CAP v2.0 -- converts @gsd-* tags, planning artifacts, and session format."
4
+ argument-hint: "[--dry-run] [--tags-only] [--force]"
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Bash
9
+ - Glob
10
+ - Grep
11
+ - AskUserQuestion
12
+ ---
13
+
14
+ <!-- @cap-feature(feature:F-MIGRATE) Migration command -- converts GSD v1.x projects to CAP v2.0 format. -->
15
+ <!-- @cap-todo Supports --dry-run, --tags-only, and --force flags. -->
16
+
17
+ <objective>
18
+ Migrate a GSD Code-First v1.x project to CAP v2.0 format. Converts @gsd-* tags to @cap-* equivalents, transforms planning artifacts into FEATURE-MAP.md entries, and migrates .planning/SESSION.json to .cap/SESSION.json.
19
+ </objective>
20
+
21
+ <context>
22
+ $ARGUMENTS
23
+
24
+ @FEATURE-MAP.md
25
+ </context>
26
+
27
+ <process>
28
+
29
+ ## Step 0: Parse flags
30
+
31
+ Check `$ARGUMENTS` for:
32
+ - `--dry-run` — show what would change without writing files
33
+ - `--tags-only` — only migrate tags, skip artifact and session migration
34
+ - `--force` — skip user confirmation gate
35
+
36
+ ## Step 1: Analyze migration scope
37
+
38
+ Run the analysis to determine what needs migrating:
39
+
40
+ ```bash
41
+ node -e "
42
+ const migrate = require('./cap/bin/lib/cap-migrate.cjs');
43
+ const report = migrate.analyzeMigration(process.cwd());
44
+ console.log(JSON.stringify(report, null, 2));
45
+ "
46
+ ```
47
+
48
+ Store as `analysis`. Present a summary to the user:
49
+
50
+ ```
51
+ Migration Analysis
52
+ ==================
53
+
54
+ @gsd-* tags found: {gsdTagCount}
55
+ Legacy artifacts: {gsdArtifacts.length} ({list})
56
+ .planning/ directory: {yes/no}
57
+ SESSION.json (v1.x): {yes/no}
58
+
59
+ Recommendations:
60
+ {For each recommendation:}
61
+ - {recommendation}
62
+ ```
63
+
64
+ ## Step 2: Confirm with user (unless --force)
65
+
66
+ If `--force` is NOT set and this is NOT `--dry-run`, use AskUserQuestion to confirm:
67
+
68
+ > "Proceed with migration? This will modify source files and create CAP v2.0 artifacts. Use --dry-run first if you want to preview changes."
69
+
70
+ If user declines, abort with message: "Migration cancelled. Run with --dry-run to preview changes."
71
+
72
+ ## Step 3: Migrate tags
73
+
74
+ ```bash
75
+ node -e "
76
+ const migrate = require('./cap/bin/lib/cap-migrate.cjs');
77
+ const dryRun = process.argv[1] === 'true';
78
+ const result = migrate.migrateTags(process.cwd(), { dryRun });
79
+ console.log(JSON.stringify(result, null, 2));
80
+ " '<DRY_RUN_VALUE>'
81
+ ```
82
+
83
+ Store as `tag_result`. Report:
84
+
85
+ ```
86
+ Tag Migration
87
+ =============
88
+ Files scanned: {filesScanned}
89
+ Files modified: {filesModified}
90
+ Tags converted: {tagsConverted}
91
+ Tags removed: {tagsRemoved}
92
+
93
+ {If changes.length > 0, show first 20 changes:}
94
+ Changes:
95
+ {file}:{line} [{action}]
96
+ - {original}
97
+ + {replaced}
98
+ ```
99
+
100
+ If `--tags-only` is set, skip to Step 6.
101
+
102
+ ## Step 4: Migrate artifacts
103
+
104
+ ```bash
105
+ node -e "
106
+ const migrate = require('./cap/bin/lib/cap-migrate.cjs');
107
+ const dryRun = process.argv[1] === 'true';
108
+ const result = migrate.migrateArtifacts(process.cwd(), { dryRun });
109
+ console.log(JSON.stringify(result, null, 2));
110
+ " '<DRY_RUN_VALUE>'
111
+ ```
112
+
113
+ Store as `artifact_result`. Report:
114
+
115
+ ```
116
+ Artifact Migration
117
+ ==================
118
+ Source: {source}
119
+ Features found: {featuresFound}
120
+ Feature Map: {featureMapCreated ? 'created/updated' : 'no changes'}
121
+ ```
122
+
123
+ ## Step 5: Migrate session
124
+
125
+ ```bash
126
+ node -e "
127
+ const migrate = require('./cap/bin/lib/cap-migrate.cjs');
128
+ const dryRun = process.argv[1] === 'true';
129
+ const result = migrate.migrateSession(process.cwd(), { dryRun });
130
+ console.log(JSON.stringify(result, null, 2));
131
+ " '<DRY_RUN_VALUE>'
132
+ ```
133
+
134
+ Store as `session_result`. Report:
135
+
136
+ ```
137
+ Session Migration
138
+ =================
139
+ Old format: {oldFormat}
140
+ New format: {newFormat}
141
+ Migrated: {migrated ? 'yes' : 'no'}
142
+ ```
143
+
144
+ ## Step 6: Final report and session update
145
+
146
+ ```
147
+ Migration Complete
148
+ ==================
149
+
150
+ Tags: {tagsConverted} converted, {tagsRemoved} removed
151
+ Artifacts: {featuresFound} features extracted → FEATURE-MAP.md
152
+ Session: {migrated ? 'migrated to .cap/SESSION.json' : 'no session to migrate'}
153
+
154
+ {If dry run:}
155
+ NOTE: This was a dry run. No files were modified. Run without --dry-run to apply changes.
156
+
157
+ {If not dry run:}
158
+ Next steps:
159
+ 1. Review changes with `git diff`
160
+ 2. Run /cap:scan to verify tag migration
161
+ 3. Run /cap:status to see project state from Feature Map
162
+ 4. Commit migrated files
163
+ ```
164
+
165
+ Update session state (unless dry run):
166
+
167
+ ```bash
168
+ node -e "
169
+ const session = require('./cap/bin/lib/cap-session.cjs');
170
+ session.updateSession(process.cwd(), {
171
+ lastCommand: '/cap:migrate',
172
+ lastCommandTimestamp: new Date().toISOString()
173
+ });
174
+ "
175
+ ```
176
+
177
+ </process>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-as-plan",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "CAP — Code as Plan. Build first, plan from code. Farley-aligned engineering framework for Claude Code.",
5
5
  "bin": {
6
6
  "cap": "bin/install.js"