@fission-ai/openspec 1.6.0-beta.1 → 1.7.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 (175) hide show
  1. package/README.md +28 -1
  2. package/dist/cli/index.js +73 -7
  3. package/dist/commands/change.d.ts +0 -1
  4. package/dist/commands/change.js +90 -50
  5. package/dist/commands/config.js +14 -8
  6. package/dist/commands/doctor.js +9 -4
  7. package/dist/commands/feedback.js +74 -23
  8. package/dist/commands/schema.js +21 -11
  9. package/dist/commands/show.js +5 -2
  10. package/dist/commands/spec.js +20 -24
  11. package/dist/commands/validate.js +18 -3
  12. package/dist/commands/workflow/index.d.ts +1 -1
  13. package/dist/commands/workflow/index.js +1 -1
  14. package/dist/commands/workflow/instructions.d.ts +7 -1
  15. package/dist/commands/workflow/instructions.js +89 -6
  16. package/dist/commands/workflow/shared.d.ts +13 -2
  17. package/dist/commands/workflow/shared.js +34 -4
  18. package/dist/commands/workflow/status.js +7 -2
  19. package/dist/core/archive.d.ts +0 -1
  20. package/dist/core/archive.js +96 -33
  21. package/dist/core/artifact-graph/graph.d.ts +16 -0
  22. package/dist/core/artifact-graph/graph.js +30 -6
  23. package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
  24. package/dist/core/artifact-graph/instruction-loader.js +55 -7
  25. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  26. package/dist/core/artifact-graph/resolver.js +34 -6
  27. package/dist/core/change-metadata/schema.d.ts +1 -0
  28. package/dist/core/change-metadata/schema.js +6 -0
  29. package/dist/core/change-status-policy.d.ts +1 -1
  30. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  31. package/dist/core/command-generation/adapters/amazon-q.js +7 -1
  32. package/dist/core/command-generation/adapters/antigravity.js +2 -1
  33. package/dist/core/command-generation/adapters/auggie.js +2 -1
  34. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  35. package/dist/core/command-generation/adapters/bob.js +6 -5
  36. package/dist/core/command-generation/adapters/claude.js +1 -8
  37. package/dist/core/command-generation/adapters/codebuddy.js +3 -2
  38. package/dist/core/command-generation/adapters/continue.js +3 -2
  39. package/dist/core/command-generation/adapters/costrict.js +2 -1
  40. package/dist/core/command-generation/adapters/crush.js +5 -5
  41. package/dist/core/command-generation/adapters/cursor.js +2 -2
  42. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  43. package/dist/core/command-generation/adapters/devin.js +36 -0
  44. package/dist/core/command-generation/adapters/factory.js +2 -1
  45. package/dist/core/command-generation/adapters/gemini.js +37 -2
  46. package/dist/core/command-generation/adapters/github-copilot.js +2 -1
  47. package/dist/core/command-generation/adapters/iflow.js +5 -4
  48. package/dist/core/command-generation/adapters/index.d.ts +2 -2
  49. package/dist/core/command-generation/adapters/index.js +2 -2
  50. package/dist/core/command-generation/adapters/junie.js +2 -1
  51. package/dist/core/command-generation/adapters/kiro.js +2 -1
  52. package/dist/core/command-generation/adapters/lingma.js +5 -5
  53. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
  54. package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
  55. package/dist/core/command-generation/adapters/opencode.js +3 -5
  56. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  57. package/dist/core/command-generation/adapters/pi.js +3 -6
  58. package/dist/core/command-generation/adapters/qoder.js +5 -5
  59. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  60. package/dist/core/command-generation/adapters/qwen.js +12 -7
  61. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  62. package/dist/core/command-generation/adapters/roocode.js +4 -4
  63. package/dist/core/command-generation/adapters/trae.js +1 -21
  64. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  65. package/dist/core/command-generation/adapters/zcode.js +33 -0
  66. package/dist/core/command-generation/generator.d.ts +8 -0
  67. package/dist/core/command-generation/generator.js +15 -1
  68. package/dist/core/command-generation/index.d.ts +1 -1
  69. package/dist/core/command-generation/index.js +1 -1
  70. package/dist/core/command-generation/invocation.d.ts +73 -0
  71. package/dist/core/command-generation/invocation.js +77 -0
  72. package/dist/core/command-generation/registry.js +4 -4
  73. package/dist/core/command-generation/types.d.ts +9 -2
  74. package/dist/core/command-generation/yaml.d.ts +20 -6
  75. package/dist/core/command-generation/yaml.js +31 -21
  76. package/dist/core/command-surface.d.ts +15 -0
  77. package/dist/core/command-surface.js +33 -0
  78. package/dist/core/completions/command-registry.js +8 -2
  79. package/dist/core/completions/generators/powershell-generator.js +15 -8
  80. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  81. package/dist/core/completions/installers/zsh-installer.js +20 -4
  82. package/dist/core/config-prompts.d.ts +1 -1
  83. package/dist/core/config-prompts.js +14 -1
  84. package/dist/core/config-schema.d.ts +7 -1
  85. package/dist/core/config-schema.js +40 -1
  86. package/dist/core/config.d.ts +12 -0
  87. package/dist/core/config.js +21 -3
  88. package/dist/core/global-config.d.ts +5 -0
  89. package/dist/core/init.d.ts +35 -0
  90. package/dist/core/init.js +225 -39
  91. package/dist/core/legacy-cleanup.d.ts +59 -0
  92. package/dist/core/legacy-cleanup.js +249 -22
  93. package/dist/core/list.js +5 -7
  94. package/dist/core/migration.d.ts +71 -1
  95. package/dist/core/migration.js +294 -1
  96. package/dist/core/onboarding-commands.d.ts +30 -0
  97. package/dist/core/onboarding-commands.js +38 -0
  98. package/dist/core/parsers/change-parser.d.ts +17 -2
  99. package/dist/core/parsers/change-parser.js +33 -20
  100. package/dist/core/parsers/code-fence.d.ts +15 -0
  101. package/dist/core/parsers/code-fence.js +49 -0
  102. package/dist/core/parsers/markdown-parser.js +2 -1
  103. package/dist/core/parsers/requirement-blocks.d.ts +8 -0
  104. package/dist/core/parsers/requirement-blocks.js +60 -35
  105. package/dist/core/parsers/requirement-text.d.ts +1 -8
  106. package/dist/core/parsers/requirement-text.js +4 -42
  107. package/dist/core/parsers/spec-structure.js +3 -29
  108. package/dist/core/profile-sync-drift.d.ts +0 -8
  109. package/dist/core/profile-sync-drift.js +9 -45
  110. package/dist/core/project-config.d.ts +25 -6
  111. package/dist/core/project-config.js +77 -7
  112. package/dist/core/references.js +42 -4
  113. package/dist/core/relationship-health.d.ts +8 -0
  114. package/dist/core/relationship-health.js +13 -0
  115. package/dist/core/root-selection.d.ts +9 -6
  116. package/dist/core/root-selection.js +42 -5
  117. package/dist/core/shared/tool-detection.d.ts +25 -5
  118. package/dist/core/shared/tool-detection.js +105 -6
  119. package/dist/core/specs-apply.d.ts +8 -33
  120. package/dist/core/specs-apply.js +252 -157
  121. package/dist/core/store/git.d.ts +13 -0
  122. package/dist/core/store/git.js +24 -0
  123. package/dist/core/templates/workflows/apply-change.js +54 -4
  124. package/dist/core/templates/workflows/archive-change.js +166 -42
  125. package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
  126. package/dist/core/templates/workflows/continue-change.js +32 -40
  127. package/dist/core/templates/workflows/explore.js +12 -0
  128. package/dist/core/templates/workflows/ff-change.js +46 -30
  129. package/dist/core/templates/workflows/new-change.js +2 -2
  130. package/dist/core/templates/workflows/onboard.js +4 -4
  131. package/dist/core/templates/workflows/propose.js +50 -32
  132. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  133. package/dist/core/templates/workflows/store-selection.js +1 -1
  134. package/dist/core/templates/workflows/sync-specs.js +172 -22
  135. package/dist/core/templates/workflows/update-change.js +20 -12
  136. package/dist/core/templates/workflows/verify-change.js +16 -10
  137. package/dist/core/update.d.ts +39 -5
  138. package/dist/core/update.js +288 -71
  139. package/dist/core/validation/constants.d.ts +4 -1
  140. package/dist/core/validation/constants.js +4 -1
  141. package/dist/core/validation/validator.d.ts +1 -7
  142. package/dist/core/validation/validator.js +92 -36
  143. package/dist/core/version-check.d.ts +152 -0
  144. package/dist/core/version-check.js +736 -0
  145. package/dist/core/view.js +12 -17
  146. package/dist/prompts/searchable-multi-select.js +1 -1
  147. package/dist/telemetry/index.d.ts +1 -1
  148. package/dist/telemetry/index.js +55 -33
  149. package/dist/ui/welcome-screen.d.ts +9 -1
  150. package/dist/ui/welcome-screen.js +91 -38
  151. package/dist/utils/change-metadata.d.ts +30 -0
  152. package/dist/utils/change-metadata.js +101 -7
  153. package/dist/utils/change-utils.d.ts +10 -4
  154. package/dist/utils/change-utils.js +20 -13
  155. package/dist/utils/command-references.d.ts +80 -6
  156. package/dist/utils/command-references.js +138 -7
  157. package/dist/utils/date.d.ts +8 -0
  158. package/dist/utils/date.js +13 -0
  159. package/dist/utils/index.d.ts +1 -1
  160. package/dist/utils/index.js +1 -1
  161. package/dist/utils/item-discovery.d.ts +19 -0
  162. package/dist/utils/item-discovery.js +30 -48
  163. package/dist/utils/shell-detection.d.ts +1 -1
  164. package/dist/utils/shell-detection.js +64 -11
  165. package/dist/utils/spec-discovery.d.ts +40 -0
  166. package/dist/utils/spec-discovery.js +102 -0
  167. package/package.json +29 -30
  168. package/schemas/spec-driven/schema.yaml +60 -5
  169. package/schemas/spec-driven/templates/design.md +2 -2
  170. package/schemas/spec-driven/templates/proposal.md +5 -1
  171. package/schemas/spec-driven/templates/spec.md +3 -0
  172. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  173. package/dist/core/command-generation/adapters/codex.js +0 -39
  174. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  175. package/dist/core/command-generation/adapters/windsurf.js +0 -38
@@ -2,6 +2,8 @@
2
2
  * Legacy cleanup module for detecting and removing OpenSpec artifacts
3
3
  * from previous init versions during the migration to the skill-based workflow.
4
4
  */
5
+ import path from 'path';
6
+ import os from 'os';
5
7
  import { promises as fs } from 'fs';
6
8
  import chalk from 'chalk';
7
9
  import { FileSystemUtils, removeMarkerBlock as removeMarkerBlockUtil } from '../utils/file-system.js';
@@ -36,7 +38,10 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
36
38
  'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
37
39
  // File-based: individual openspec-*.md files in a commands/workflows/prompts folder
38
40
  'cursor': { type: 'files', pattern: '.cursor/commands/openspec-*.md' },
39
- 'windsurf': { type: 'files', pattern: '.windsurf/workflows/openspec-*.md' },
41
+ // Keyed by the tool id these map back to, so the pre-opsx Windsurf files
42
+ // belong to `devin` — the id Windsurf became. Only `.windsurf/` is listed:
43
+ // `.devin/` postdates the opsx rename and never held `openspec-*` files.
44
+ 'devin': { type: 'files', pattern: '.windsurf/workflows/openspec-*.md' },
40
45
  'kilocode': { type: 'files', pattern: '.kilocode/workflows/openspec-*.md' },
41
46
  'kiro': { type: 'files', pattern: '.kiro/prompts/openspec-*.prompt.md' },
42
47
  'github-copilot': { type: 'files', pattern: '.github/prompts/openspec-*.prompt.md' },
@@ -50,9 +55,83 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
50
55
  'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
51
56
  'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
52
57
  'junie': { type: 'files', pattern: ['.junie/commands/opsx-*.md', '.junie/commands/openspec-*.md'] },
53
- 'qwen': { type: 'files', pattern: '.qwen/commands/openspec-*.toml' },
58
+ 'qwen': { type: 'files', pattern: ['.qwen/commands/opsx-*.toml', '.qwen/commands/openspec-*.toml'] },
54
59
  'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
55
60
  };
61
+ /**
62
+ * Final OpenSpec-managed global Codex prompt filenames mapped to the workflows
63
+ * they represented before Codex moved to skills-only delivery.
64
+ */
65
+ const LEGACY_GLOBAL_CODEX_WORKFLOWS = {
66
+ 'opsx-propose.md': ['propose'],
67
+ 'opsx-explore.md': ['explore'],
68
+ 'opsx-new.md': ['new'],
69
+ 'opsx-continue.md': ['continue'],
70
+ 'opsx-apply.md': ['apply'],
71
+ 'opsx-update.md': ['update'],
72
+ 'opsx-ff.md': ['ff'],
73
+ 'opsx-sync.md': ['sync'],
74
+ 'opsx-archive.md': ['archive'],
75
+ 'opsx-bulk-archive.md': ['bulk-archive'],
76
+ 'opsx-verify.md': ['verify'],
77
+ 'opsx-onboard.md': ['onboard'],
78
+ };
79
+ /**
80
+ * Global legacy prompt locations that live outside the project tree and require
81
+ * allowlisted matching instead of broad glob-based cleanup.
82
+ */
83
+ export const LEGACY_GLOBAL_SLASH_COMMAND_PATHS = {
84
+ 'codex': {
85
+ managedFileNames: Object.keys(LEGACY_GLOBAL_CODEX_WORKFLOWS),
86
+ workflowIdsByFileName: LEGACY_GLOBAL_CODEX_WORKFLOWS,
87
+ resolvePromptDir: getCodexPromptDir,
88
+ replacementLabel: 'Codex skills',
89
+ },
90
+ };
91
+ // Resolve the Codex global prompts directory, respecting CODEX_HOME if set.
92
+ export function getCodexPromptDir() {
93
+ const envHome = process.env.CODEX_HOME?.trim();
94
+ const codexHome = envHome ? envHome : path.join(os.homedir(), '.codex');
95
+ return path.join(path.resolve(codexHome), 'prompts');
96
+ }
97
+ // Convert a simple glob pattern (only * wildcards) into an anchored RegExp.
98
+ function globToRegex(pattern) {
99
+ const regexPattern = pattern
100
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
101
+ .replace(/\*/g, '.*');
102
+ return new RegExp(`^${regexPattern}$`);
103
+ }
104
+ // Normalize Windows backslashes to forward slashes for cross-platform path matching.
105
+ function normalizePathForMatch(filePath) {
106
+ return filePath.replace(/\\/g, '/');
107
+ }
108
+ /**
109
+ * Classifies a global Codex prompt path as OpenSpec-managed only when it matches
110
+ * the explicit legacy allowlist for the resolved prompt home.
111
+ */
112
+ function getManagedGlobalLegacyPromptMetadata(filePath) {
113
+ if (!path.isAbsolute(filePath)) {
114
+ return undefined;
115
+ }
116
+ const resolvedPath = path.resolve(filePath);
117
+ for (const [toolId, pattern] of Object.entries(LEGACY_GLOBAL_SLASH_COMMAND_PATHS)) {
118
+ const promptDir = path.resolve(pattern.resolvePromptDir());
119
+ if (path.dirname(resolvedPath) !== promptDir) {
120
+ continue;
121
+ }
122
+ const managedFileName = path.basename(resolvedPath);
123
+ if (pattern.managedFileNames.includes(managedFileName)) {
124
+ return {
125
+ path: resolvedPath,
126
+ toolId,
127
+ managedFileName,
128
+ workflowIds: pattern.workflowIdsByFileName?.[managedFileName] ?? [],
129
+ replacementLabel: pattern.replacementLabel,
130
+ };
131
+ }
132
+ }
133
+ return undefined;
134
+ }
56
135
  /**
57
136
  * Detects all legacy OpenSpec artifacts in a project.
58
137
  *
@@ -65,6 +144,8 @@ export async function detectLegacyArtifacts(projectPath) {
65
144
  configFilesToUpdate: [],
66
145
  slashCommandDirs: [],
67
146
  slashCommandFiles: [],
147
+ globalSlashCommandFiles: [],
148
+ globalSlashCommandDetails: [],
68
149
  hasOpenspecAgents: false,
69
150
  hasProjectMd: false,
70
151
  hasRootAgentsWithMarkers: false,
@@ -77,7 +158,10 @@ export async function detectLegacyArtifacts(projectPath) {
77
158
  // Detect legacy slash commands
78
159
  const slashResult = await detectLegacySlashCommands(projectPath);
79
160
  result.slashCommandDirs = slashResult.directories;
80
- result.slashCommandFiles = slashResult.files;
161
+ result.slashCommandFiles = [...new Set(slashResult.files)];
162
+ // Detect legacy global slash commands
163
+ result.globalSlashCommandDetails = await detectLegacyGlobalPromptFiles();
164
+ result.globalSlashCommandFiles = result.globalSlashCommandDetails.map((detail) => detail.path);
81
165
  // Detect legacy structure files
82
166
  const structureResult = await detectLegacyStructureFiles(projectPath);
83
167
  result.hasOpenspecAgents = structureResult.hasOpenspecAgents;
@@ -88,6 +172,7 @@ export async function detectLegacyArtifacts(projectPath) {
88
172
  result.configFiles.length > 0 ||
89
173
  result.slashCommandDirs.length > 0 ||
90
174
  result.slashCommandFiles.length > 0 ||
175
+ result.globalSlashCommandFiles.length > 0 ||
91
176
  result.hasOpenspecAgents ||
92
177
  result.hasRootAgentsWithMarkers ||
93
178
  result.hasProjectMd;
@@ -125,7 +210,7 @@ export async function detectLegacyConfigFiles(projectPath) {
125
210
  export async function detectLegacySlashCommands(projectPath) {
126
211
  const directories = [];
127
212
  const files = [];
128
- for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
213
+ for (const pattern of Object.values(LEGACY_SLASH_COMMAND_PATHS)) {
129
214
  if (pattern.type === 'directory' && pattern.path) {
130
215
  const dirPath = FileSystemUtils.joinPath(projectPath, pattern.path);
131
216
  if (await FileSystemUtils.directoryExists(dirPath)) {
@@ -133,7 +218,6 @@ export async function detectLegacySlashCommands(projectPath) {
133
218
  }
134
219
  }
135
220
  else if (pattern.type === 'files' && pattern.pattern) {
136
- // For file-based patterns, check for individual files
137
221
  const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
138
222
  for (const p of patterns) {
139
223
  const foundFiles = await findLegacySlashCommandFiles(projectPath, p);
@@ -143,6 +227,37 @@ export async function detectLegacySlashCommands(projectPath) {
143
227
  }
144
228
  return { directories, files };
145
229
  }
230
+ /**
231
+ * Detects legacy global slash command files.
232
+ *
233
+ * @returns Object with individual files found
234
+ */
235
+ /**
236
+ * Scans the resolved global Codex prompt directories and returns only the
237
+ * allowlisted OpenSpec-managed legacy prompt files.
238
+ */
239
+ async function detectLegacyGlobalPromptFiles() {
240
+ const foundFiles = [];
241
+ for (const pattern of Object.values(LEGACY_GLOBAL_SLASH_COMMAND_PATHS)) {
242
+ const promptDir = pattern.resolvePromptDir();
243
+ try {
244
+ const entries = await fs.readdir(promptDir, { withFileTypes: true });
245
+ for (const entry of entries) {
246
+ if (entry.isFile() && pattern.managedFileNames.includes(entry.name)) {
247
+ const fullPath = path.join(promptDir, entry.name);
248
+ const match = getManagedGlobalLegacyPromptMetadata(fullPath);
249
+ if (match) {
250
+ foundFiles.push(match);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ catch {
256
+ // Directory does not exist or cannot be read.
257
+ }
258
+ }
259
+ return foundFiles;
260
+ }
146
261
  /**
147
262
  * Finds legacy slash command files matching a glob pattern.
148
263
  *
@@ -165,14 +280,7 @@ async function findLegacySlashCommandFiles(projectPath, pattern) {
165
280
  }
166
281
  try {
167
282
  const entries = await fs.readdir(dirPath);
168
- // Convert glob pattern to regex
169
- // openspec-*.md -> /^openspec-.*\.md$/
170
- // openspec-*.prompt.md -> /^openspec-.*\.prompt\.md$/
171
- // openspec-*.toml -> /^openspec-.*\.toml$/
172
- const regexPattern = filePart
173
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
174
- .replace(/\*/g, '.*'); // Replace * with .*
175
- const regex = new RegExp(`^${regexPattern}$`);
283
+ const regex = globToRegex(filePart);
176
284
  for (const entry of entries) {
177
285
  if (regex.test(entry)) {
178
286
  // Use forward slashes for consistency in relative paths (cross-platform)
@@ -257,6 +365,7 @@ export function removeMarkerBlock(content) {
257
365
  export async function cleanupLegacyArtifacts(projectPath, detection) {
258
366
  const result = {
259
367
  deletedFiles: [],
368
+ deletedFileReplacementLabels: {},
260
369
  modifiedFiles: [],
261
370
  deletedDirs: [],
262
371
  projectMdNeedsMigration: detection.hasProjectMd,
@@ -299,6 +408,25 @@ export async function cleanupLegacyArtifacts(projectPath, detection) {
299
408
  result.errors.push(`Failed to delete ${filePath}: ${error.message}`);
300
409
  }
301
410
  }
411
+ // Delete managed global slash command files (these are 100% OpenSpec-managed)
412
+ const globalPromptMatchesByPath = new Map(getLegacyGlobalPromptMatches(detection).map((prompt) => [prompt.path, prompt]));
413
+ for (const filePath of detection.globalSlashCommandFiles) {
414
+ if (!getManagedGlobalLegacyPromptMetadata(filePath)) {
415
+ result.errors.push(`Skipped unmanaged global prompt ${filePath}`);
416
+ continue;
417
+ }
418
+ try {
419
+ await fs.unlink(filePath);
420
+ result.deletedFiles.push(filePath);
421
+ const promptMatch = globalPromptMatchesByPath.get(filePath);
422
+ if (promptMatch?.replacementLabel) {
423
+ result.deletedFileReplacementLabels[filePath] = promptMatch.replacementLabel;
424
+ }
425
+ }
426
+ catch (error) {
427
+ result.errors.push(`Failed to delete ${filePath}: ${error.message}`);
428
+ }
429
+ }
302
430
  // Delete openspec/AGENTS.md (this is inside openspec/, it's OpenSpec-managed)
303
431
  if (detection.hasOpenspecAgents) {
304
432
  const agentsPath = FileSystemUtils.joinPath(projectPath, 'openspec', 'AGENTS.md');
@@ -328,10 +456,15 @@ export function formatCleanupSummary(result) {
328
456
  if (result.deletedFiles.length > 0 || result.deletedDirs.length > 0 || result.modifiedFiles.length > 0) {
329
457
  lines.push('Cleaned up legacy files:');
330
458
  for (const file of result.deletedFiles) {
331
- lines.push(` ✓ Removed ${file}`);
459
+ const replacementLabel = result.deletedFileReplacementLabels?.[file]
460
+ ?? getManagedGlobalLegacyPromptMetadata(file)?.replacementLabel;
461
+ const replacement = replacementLabel
462
+ ? ` (replaced by ${replacementLabel})`
463
+ : '';
464
+ lines.push(` ✓ Removed ${file}${replacement}`);
332
465
  }
333
466
  for (const dir of result.deletedDirs) {
334
- lines.push(` ✓ Removed ${dir}/ (replaced by /opsx:*)`);
467
+ lines.push(` ✓ Removed ${dir}/ (replaced by OpenSpec skills and commands)`);
335
468
  }
336
469
  for (const file of result.modifiedFiles) {
337
470
  lines.push(` ✓ Removed OpenSpec markers from ${file}`);
@@ -374,6 +507,13 @@ function buildRemovalsList(detection) {
374
507
  for (const file of detection.slashCommandFiles) {
375
508
  removals.push({ path: file, explanation: 'replaced by skills/' });
376
509
  }
510
+ // Managed global slash command files
511
+ for (const prompt of getLegacyGlobalPromptMatches(detection)) {
512
+ const explanation = prompt.toolId
513
+ ? `replaced by .${prompt.toolId}/skills/`
514
+ : 'replaced by skills/';
515
+ removals.push({ path: prompt.path, explanation });
516
+ }
377
517
  // openspec/AGENTS.md (inside openspec/, it's OpenSpec-managed)
378
518
  if (detection.hasOpenspecAgents) {
379
519
  removals.push({ path: 'openspec/AGENTS.md', explanation: 'obsolete workflow file' });
@@ -445,6 +585,24 @@ export function formatDetectionSummary(detection) {
445
585
  }
446
586
  return lines.join('\n');
447
587
  }
588
+ /**
589
+ * Generates a summary for managed global prompt files whose cleanup must wait
590
+ * until replacement skills are installed.
591
+ */
592
+ export function formatDeferredGlobalPromptSummary(detection) {
593
+ const deferredPrompts = getLegacyGlobalPromptMatches(detection);
594
+ if (deferredPrompts.length === 0) {
595
+ return '';
596
+ }
597
+ const lines = [];
598
+ lines.push(chalk.bold('Deferred global prompts cleanup'));
599
+ lines.push(chalk.dim('These global prompts will only be removed after matching replacement skills are installed.'));
600
+ for (const prompt of deferredPrompts) {
601
+ const toolLabel = prompt.toolId ? `${prompt.toolId}: ` : '';
602
+ lines.push(` • ${toolLabel}${prompt.path}`);
603
+ }
604
+ return lines.join('\n');
605
+ }
448
606
  /**
449
607
  * Extract tool IDs from detected legacy artifacts.
450
608
  * Uses LEGACY_SLASH_COMMAND_PATHS to map paths back to tool IDs.
@@ -466,18 +624,13 @@ export function getToolsFromLegacyArtifacts(detection) {
466
624
  // Match files to tool IDs using glob patterns
467
625
  for (const file of detection.slashCommandFiles) {
468
626
  // Normalize file path to use forward slashes for consistent matching (Windows compatibility)
469
- const normalizedFile = file.replace(/\\/g, '/');
627
+ const normalizedFile = normalizePathForMatch(file);
470
628
  for (const [toolId, pattern] of Object.entries(LEGACY_SLASH_COMMAND_PATHS)) {
471
629
  if (pattern.type === 'files' && pattern.pattern) {
472
- // Convert glob pattern to regex for matching
473
- // e.g., '.cursor/commands/openspec-*.md' -> /^\.cursor\/commands\/openspec-.*\.md$/
474
630
  const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
475
631
  let matched = false;
476
632
  for (const p of patterns) {
477
- const regexPattern = p
478
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
479
- .replace(/\*/g, '.*'); // Replace * with .*
480
- const regex = new RegExp(`^${regexPattern}$`);
633
+ const regex = globToRegex(p);
481
634
  if (regex.test(normalizedFile)) {
482
635
  tools.add(toolId);
483
636
  matched = true;
@@ -489,8 +642,82 @@ export function getToolsFromLegacyArtifacts(detection) {
489
642
  }
490
643
  }
491
644
  }
645
+ for (const prompt of getLegacyGlobalPromptMatches(detection)) {
646
+ tools.add(prompt.toolId);
647
+ }
492
648
  return Array.from(tools);
493
649
  }
650
+ /**
651
+ * Normalizes global Codex prompt matches so callers can rely on workflow-aware
652
+ * metadata even when older detection results only carry file paths.
653
+ */
654
+ export function getLegacyGlobalPromptMatches(detection) {
655
+ if (detection.globalSlashCommandDetails && detection.globalSlashCommandDetails.length > 0) {
656
+ return detection.globalSlashCommandDetails;
657
+ }
658
+ return detection.globalSlashCommandFiles
659
+ .map((filePath) => getManagedGlobalLegacyPromptMetadata(filePath))
660
+ .filter((match) => match !== undefined);
661
+ }
662
+ /**
663
+ * Collects workflow IDs inferred from detected legacy global prompts for a
664
+ * specific tool.
665
+ */
666
+ export function getLegacyWorkflowIdsForTool(detection, toolId) {
667
+ const workflows = new Set();
668
+ for (const prompt of getLegacyGlobalPromptMatches(detection)) {
669
+ if (prompt.toolId !== toolId) {
670
+ continue;
671
+ }
672
+ for (const workflowId of prompt.workflowIds) {
673
+ workflows.add(workflowId);
674
+ }
675
+ }
676
+ return Array.from(workflows);
677
+ }
678
+ function hasLegacyArtifacts(detection) {
679
+ return (detection.configFiles.length > 0 ||
680
+ detection.slashCommandDirs.length > 0 ||
681
+ detection.slashCommandFiles.length > 0 ||
682
+ detection.globalSlashCommandFiles.length > 0 ||
683
+ detection.hasOpenspecAgents ||
684
+ detection.hasRootAgentsWithMarkers ||
685
+ detection.hasProjectMd);
686
+ }
687
+ /**
688
+ * Returns a detection snapshot with global Codex prompt cleanup removed so
689
+ * callers can safely perform the immediate, non-deferred cleanup pass.
690
+ */
691
+ export function omitGlobalLegacyPromptFiles(detection) {
692
+ const nextDetection = {
693
+ ...detection,
694
+ globalSlashCommandFiles: [],
695
+ globalSlashCommandDetails: [],
696
+ };
697
+ nextDetection.hasLegacyArtifacts = hasLegacyArtifacts(nextDetection);
698
+ return nextDetection;
699
+ }
700
+ /**
701
+ * Builds a detection snapshot containing only the selected global Codex prompt
702
+ * matches for replacement-gated cleanup.
703
+ */
704
+ export function pickGlobalLegacyPromptFiles(detection, filePaths) {
705
+ const selectedPaths = new Set(filePaths.map((filePath) => path.resolve(filePath)));
706
+ const details = getLegacyGlobalPromptMatches(detection)
707
+ .filter((detail) => selectedPaths.has(path.resolve(detail.path)));
708
+ return {
709
+ configFiles: [],
710
+ configFilesToUpdate: [],
711
+ slashCommandDirs: [],
712
+ slashCommandFiles: [],
713
+ globalSlashCommandFiles: details.map((detail) => detail.path),
714
+ globalSlashCommandDetails: details,
715
+ hasOpenspecAgents: false,
716
+ hasProjectMd: false,
717
+ hasRootAgentsWithMarkers: false,
718
+ hasLegacyArtifacts: details.length > 0,
719
+ };
720
+ }
494
721
  /**
495
722
  * Generates a migration hint message for project.md.
496
723
  * This is shown when project.md exists and needs manual migration to config.yaml.
package/dist/core/list.js CHANGED
@@ -2,8 +2,8 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
4
4
  import { readFileSync } from 'fs';
5
- import { join } from 'path';
6
5
  import { MarkdownParser } from './parsers/markdown-parser.js';
6
+ import { discoverSpecFiles } from '../utils/spec-discovery.js';
7
7
  function isMissingPathError(error) {
8
8
  return (typeof error === 'object' &&
9
9
  error !== null &&
@@ -152,9 +152,8 @@ export class ListCommand {
152
152
  }
153
153
  return;
154
154
  }
155
- const entries = await fs.readdir(specsDir, { withFileTypes: true });
156
- const specDirs = entries.filter(e => e.isDirectory()).map(e => e.name);
157
- if (specDirs.length === 0) {
155
+ const discovered = await discoverSpecFiles(specsDir);
156
+ if (discovered.length === 0) {
158
157
  if (json) {
159
158
  console.log(JSON.stringify({ specs: [], ...(root ? { root } : {}) }, null, 2));
160
159
  }
@@ -164,10 +163,9 @@ export class ListCommand {
164
163
  return;
165
164
  }
166
165
  const specs = [];
167
- for (const id of specDirs) {
168
- const specPath = join(specsDir, id, 'spec.md');
166
+ for (const { id, specFile } of discovered) {
169
167
  try {
170
- const content = readFileSync(specPath, 'utf-8');
168
+ const content = readFileSync(specFile, 'utf-8');
171
169
  const parser = new MarkdownParser(content);
172
170
  const spec = parser.parseSpec(id);
173
171
  specs.push({ id, requirementCount: spec.requirements.length });
@@ -4,7 +4,77 @@
4
4
  * One-time migration logic for existing projects when profile system is introduced.
5
5
  * Called by both init and update commands before profile resolution.
6
6
  */
7
- import type { AIToolOption } from './config.js';
7
+ import { type AIToolOption } from './config.js';
8
+ export interface LegacyToolRoot {
9
+ /** Former tool root, e.g. '.kimi' */
10
+ root: string;
11
+ /**
12
+ * Whether leaving this root requires the user's say-so. False when the old
13
+ * product is gone and its directory is certainly dead. True when the old
14
+ * location may still be the live one for somebody.
15
+ */
16
+ needsConsent: boolean;
17
+ }
18
+ /**
19
+ * Former tool roots whose OpenSpec-managed content belongs under the tool's
20
+ * current skillsDir. User files are never touched.
21
+ */
22
+ export declare const LEGACY_TOOL_ROOTS: Record<string, LegacyToolRoot[]>;
23
+ export interface LegacyToolMigration {
24
+ toolId: string;
25
+ /** Legacy tool root, e.g. '.windsurf' */
26
+ from: string;
27
+ /** Current tool root, e.g. '.devin' */
28
+ to: string;
29
+ /** Skill directories that moved, or would move */
30
+ skillDirs: number;
31
+ /** Command files that moved, or would move */
32
+ commandFiles: number;
33
+ /**
34
+ * OpenSpec-managed files left under the legacy root because the copy there
35
+ * differs from the one that survives — the user edited it, so it is reported
36
+ * rather than dropped.
37
+ */
38
+ keptInPlace: number;
39
+ /** Whether this move needs the user's consent first */
40
+ needsConsent: boolean;
41
+ }
42
+ /**
43
+ * Reports the OpenSpec content sitting under each tool's legacy root, without
44
+ * moving anything. Callers use this to ask before a move that needs consent.
45
+ */
46
+ export declare function findLegacyToolMigrations(projectPath: string): LegacyToolMigration[];
47
+ /**
48
+ * Moves OpenSpec-managed skill directories (openspec-*) and command files
49
+ * (opsx-*) from a tool's legacy root to its current one. When the destination
50
+ * already exists the legacy copy is removed instead. Legacy directories are
51
+ * deleted only when left empty, so user files under the old location — a
52
+ * hand-written Cascade workflow next to the generated ones — are preserved.
53
+ *
54
+ * @param projectPath - Project root
55
+ * @param toolIds - Restrict the move to these tools; omit to move every tool
56
+ * whose legacy root needs no consent
57
+ */
58
+ export declare function migrateLegacyToolDirs(projectPath: string, toolIds?: string[]): LegacyToolMigration[];
59
+ /**
60
+ * Summarizes what a migration moved, e.g. "6 skills and 6 commands".
61
+ */
62
+ export declare function describeLegacyMigration(migration: LegacyToolMigration): string;
63
+ /**
64
+ * Names OpenSpec-managed files the move deliberately left behind, so a user
65
+ * who customized one knows there are now two copies to reconcile.
66
+ */
67
+ export declare function keptInPlaceNotice(migration: LegacyToolMigration): string | undefined;
68
+ /**
69
+ * Whether a migration has anything to move, as opposed to only files left in
70
+ * place. Callers use this to avoid offering a move of nothing.
71
+ */
72
+ export declare function hasMovableContent(migration: LegacyToolMigration): boolean;
73
+ /**
74
+ * Explains why a consent-gated move is being offered, in the user's terms.
75
+ * Keyed by tool so the reason is specific rather than a generic "files moved".
76
+ */
77
+ export declare function legacyMigrationNotice(migration: LegacyToolMigration): string;
8
78
  /**
9
79
  * Scans installed workflow files across all detected tools and returns
10
80
  * the union of installed workflow IDs.