@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
package/dist/core/init.js CHANGED
@@ -12,18 +12,19 @@ import { createRequire } from 'module';
12
12
  import { FileSystemUtils } from '../utils/file-system.js';
13
13
  import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
14
14
  import { findRepoPlanningRootSync } from './planning-home.js';
15
- import { transformToHyphenCommands } from '../utils/command-references.js';
16
- import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
15
+ import { getSkillReferenceTransformer, getTransformerForTool } from '../utils/command-references.js';
16
+ import { AI_TOOLS, OPENSPEC_DIR_NAME, resolveToolIdAlias, } from './config.js';
17
17
  import { PALETTE } from './styles/palette.js';
18
18
  import { isInteractive } from '../utils/interactive.js';
19
19
  import { serializeConfig } from './config-prompts.js';
20
20
  import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
21
- import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, } from './legacy-cleanup.js';
21
+ import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDeferredGlobalPromptSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
22
22
  import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
23
23
  import { getGlobalConfig } from './global-config.js';
24
24
  import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
25
25
  import { getAvailableTools } from './available-tools.js';
26
- import { migrateIfNeeded } from './migration.js';
26
+ import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
27
+ import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
27
28
  const require = createRequire(import.meta.url);
28
29
  const { version: OPENSPEC_VERSION } = require('../../package.json');
29
30
  // -----------------------------------------------------------------------------
@@ -56,11 +57,13 @@ export class InitCommand {
56
57
  force;
57
58
  interactiveOption;
58
59
  profileOverride;
60
+ animation;
59
61
  constructor(options = {}) {
60
62
  this.toolsArg = options.tools;
61
63
  this.force = options.force ?? false;
62
64
  this.interactiveOption = options.interactive;
63
65
  this.profileOverride = options.profile;
66
+ this.animation = options.animation ?? true;
64
67
  }
65
68
  async execute(targetPath) {
66
69
  const projectPath = path.resolve(targetPath);
@@ -91,32 +94,53 @@ export class InitCommand {
91
94
  }
92
95
  }
93
96
  // Check for legacy artifacts and handle cleanup
94
- await this.handleLegacyCleanup(projectPath, extendMode);
97
+ const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
98
+ // Migrate OpenSpec-managed skills left in renamed tool directories
99
+ // (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
100
+ migrateLegacyToolDirs(projectPath);
95
101
  // Detect available tools in the project (task 7.1)
96
102
  const detectedTools = getAvailableTools(projectPath);
97
103
  // Migration check: migrate existing projects to profile system (task 7.3)
98
104
  if (extendMode) {
99
105
  migrateIfNeeded(projectPath, detectedTools);
100
106
  }
107
+ // Validate profile override early so invalid values fail before tool setup.
108
+ // The resolved value is consumed later when generation reads effective config.
109
+ // This runs ahead of the welcome screen so an invalid --profile does not make
110
+ // the user press Enter before seeing the error.
111
+ this.resolveProfileOverride();
101
112
  // Show animated welcome screen (interactive mode only)
102
113
  const canPrompt = this.canPromptInteractively();
103
114
  if (canPrompt) {
104
115
  const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
105
- await showWelcomeScreen();
116
+ await showWelcomeScreen(this.getActiveWorkflows(), { animate: this.animation });
106
117
  }
107
- // Validate profile override early so invalid values fail before tool setup.
108
- // The resolved value is consumed later when generation reads effective config.
109
- this.resolveProfileOverride();
110
118
  // Get tool states before processing
111
119
  const toolStates = getToolStates(projectPath);
112
120
  // Get tool selection (pass detected tools for pre-selection)
113
121
  const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
114
122
  // Validate selected tools
115
123
  const validatedTools = this.validateTools(selectedToolIds, toolStates);
124
+ // Selecting a renamed tool is consent to leave its former directory:
125
+ // init is about to write the current one, and leaving OpenSpec content
126
+ // behind would give the user two installs of the same tool.
127
+ for (const migration of migrateLegacyToolDirs(projectPath, validatedTools.map((tool) => tool.value))) {
128
+ if (hasMovableContent(migration)) {
129
+ console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
130
+ }
131
+ const kept = keptInPlaceNotice(migration);
132
+ if (kept)
133
+ console.log(chalk.dim(kept));
134
+ }
116
135
  // Create directory structure and config
117
136
  await this.createDirectoryStructure(openspecPath, extendMode);
118
137
  // Generate skills and commands for each tool
119
138
  const results = await this.generateSkillsAndCommands(projectPath, validatedTools);
139
+ // Legacy cleanup was deferred to avoid interfering with skill/command generation;
140
+ // now that outputs are written, finalize the cleanup (e.g. remove stale files).
141
+ if (deferredLegacyCleanup) {
142
+ await this.finalizeDeferredLegacyCleanup(projectPath, deferredLegacyCleanup);
143
+ }
120
144
  // Create config.yaml if needed
121
145
  const configStatus = await this.createConfig(openspecPath, extendMode);
122
146
  // Display success message
@@ -149,26 +173,50 @@ export class InitCommand {
149
173
  }
150
174
  throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`);
151
175
  }
176
+ /**
177
+ * Resolves the workflows the effective profile installs, so onboarding output
178
+ * only mentions commands that will actually exist.
179
+ */
180
+ getActiveWorkflows() {
181
+ const globalCfg = getGlobalConfig();
182
+ const activeProfile = this.resolveProfileOverride() ?? globalCfg.profile ?? 'core';
183
+ return [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
184
+ }
152
185
  // ═══════════════════════════════════════════════════════════
153
186
  // LEGACY CLEANUP
154
187
  // ═══════════════════════════════════════════════════════════
188
+ /**
189
+ * Cleans repo-local legacy artifacts immediately and defers global Codex prompt
190
+ * cleanup until replacement skills have been installed.
191
+ */
155
192
  async handleLegacyCleanup(projectPath, extendMode) {
156
193
  // Detect legacy artifacts
157
194
  const detection = await detectLegacyArtifacts(projectPath);
158
195
  if (!detection.hasLegacyArtifacts) {
159
- return; // No legacy artifacts found
196
+ return null; // No legacy artifacts found
160
197
  }
198
+ const immediateDetection = omitGlobalLegacyPromptFiles(detection);
161
199
  // Show what was detected
162
- console.log();
163
- console.log(formatDetectionSummary(detection));
164
- console.log();
200
+ const immediateSummary = formatDetectionSummary(immediateDetection);
201
+ if (immediateSummary) {
202
+ console.log();
203
+ console.log(immediateSummary);
204
+ console.log();
205
+ }
206
+ // Show which global prompts are deferred — they'll only be removed once
207
+ // the corresponding replacement skills are installed during generation.
208
+ const deferredSummary = formatDeferredGlobalPromptSummary(detection);
209
+ if (deferredSummary) {
210
+ console.log(deferredSummary);
211
+ console.log();
212
+ }
165
213
  const canPrompt = this.canPromptInteractively();
166
214
  if (this.force || !canPrompt) {
167
215
  // --force flag or non-interactive mode: proceed with cleanup automatically.
168
216
  // Legacy slash commands are 100% OpenSpec-managed, and config file cleanup
169
217
  // only removes markers (never deletes files), so auto-cleanup is safe.
170
- await this.performLegacyCleanup(projectPath, detection);
171
- return;
218
+ await this.performImmediateLegacyCleanup(projectPath, detection);
219
+ return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
172
220
  }
173
221
  // Interactive mode: prompt for confirmation
174
222
  const { confirm } = await import('@inquirer/prompts');
@@ -181,7 +229,51 @@ export class InitCommand {
181
229
  console.log(chalk.dim('Run with --force to skip this prompt, or manually remove legacy files.'));
182
230
  process.exit(0);
183
231
  }
184
- await this.performLegacyCleanup(projectPath, detection);
232
+ await this.performImmediateLegacyCleanup(projectPath, detection);
233
+ return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
234
+ }
235
+ /**
236
+ * Applies the safe subset of legacy cleanup that does not depend on newly
237
+ * generated Codex skills.
238
+ */
239
+ async performImmediateLegacyCleanup(projectPath, detection) {
240
+ const immediateDetection = omitGlobalLegacyPromptFiles(detection);
241
+ if (!immediateDetection.hasLegacyArtifacts) {
242
+ return;
243
+ }
244
+ await this.performLegacyCleanup(projectPath, immediateDetection);
245
+ }
246
+ /**
247
+ * Removes only the legacy global Codex prompts whose workflows now have
248
+ * replacement skills in the project.
249
+ */
250
+ async finalizeDeferredLegacyCleanup(projectPath, deferredCleanup) {
251
+ const availableCodexWorkflows = await this.getInstalledWorkflowsForTool(projectPath, 'codex');
252
+ const removableMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
253
+ .filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
254
+ if (removableMatches.length > 0) {
255
+ await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(deferredCleanup.detection, removableMatches.map((prompt) => prompt.path)));
256
+ }
257
+ const blockedMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
258
+ .filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
259
+ if (blockedMatches.length > 0) {
260
+ console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
261
+ for (const prompt of blockedMatches) {
262
+ console.log(chalk.dim(` - ${prompt.toolId}: ${prompt.path}`));
263
+ }
264
+ console.log();
265
+ }
266
+ }
267
+ /**
268
+ * Reads the currently installed workflow IDs for a single tool from the
269
+ * generated skill layout on disk.
270
+ */
271
+ async getInstalledWorkflowsForTool(projectPath, toolId) {
272
+ const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
273
+ if (!tool) {
274
+ return new Set();
275
+ }
276
+ return new Set(scanInstalledWorkflowsShared(projectPath, [tool]));
185
277
  }
186
278
  async performLegacyCleanup(projectPath, detection) {
187
279
  const spinner = ora('Cleaning up legacy files...').start();
@@ -300,7 +392,9 @@ export class InitCommand {
300
392
  if (tokens.length === 0) {
301
393
  throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
302
394
  }
303
- const normalizedTokens = tokens.map((token) => token.toLowerCase());
395
+ // Retired ids resolve to their current tool, so a rebrand does not break
396
+ // an existing `--tools windsurf` in someone's setup script.
397
+ const normalizedTokens = tokens.map((token) => resolveToolIdAlias(token.toLowerCase()));
304
398
  if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
305
399
  throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
306
400
  }
@@ -374,11 +468,20 @@ export class InitCommand {
374
468
  // ═══════════════════════════════════════════════════════════
375
469
  // SKILL & COMMAND GENERATION
376
470
  // ═══════════════════════════════════════════════════════════
471
+ /**
472
+ * Generates skill files and slash commands for each selected tool,
473
+ * honoring the configured delivery mode (skills, commands, or both).
474
+ *
475
+ * @param projectPath - Absolute path to the project root
476
+ * @param tools - Selected tools with their skill directory metadata
477
+ * @returns Created, refreshed, and failed tools plus removed artifact counts
478
+ */
377
479
  async generateSkillsAndCommands(projectPath, tools) {
378
480
  const createdTools = [];
379
481
  const refreshedTools = [];
380
482
  const failedTools = [];
381
483
  const commandsSkipped = [];
484
+ const skillsInvocableCommandSkips = [];
382
485
  let removedCommandCount = 0;
383
486
  let removedSkillCount = 0;
384
487
  // Read global config for profile and delivery settings (use --profile override if set)
@@ -387,15 +490,16 @@ export class InitCommand {
387
490
  const delivery = globalConfig.delivery ?? 'both';
388
491
  const workflows = getProfileWorkflows(profile, globalConfig.workflows);
389
492
  // Get skill and command templates filtered by profile workflows
390
- const shouldGenerateSkills = delivery !== 'commands';
391
- const shouldGenerateCommands = delivery !== 'skills';
392
- const skillTemplates = shouldGenerateSkills ? getSkillTemplates(workflows) : [];
393
- const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : [];
493
+ const deliveryIncludesCommands = delivery !== 'skills';
494
+ const skillTemplates = getSkillTemplates(workflows);
495
+ const commandContents = getCommandContents(workflows);
394
496
  // Process each tool
395
497
  for (const tool of tools) {
396
498
  const spinner = ora(`Setting up ${tool.name}...`).start();
397
499
  try {
398
- // Generate skill files if delivery includes skills
500
+ const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
501
+ const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
502
+ // Generate skill files if the selected delivery and tool capability allow skills
399
503
  if (shouldGenerateSkills) {
400
504
  // Use tool-specific skillsDir
401
505
  const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
@@ -404,14 +508,13 @@ export class InitCommand {
404
508
  const skillDir = path.join(skillsDir, dirName);
405
509
  const skillFile = path.join(skillDir, 'SKILL.md');
406
510
  // Generate SKILL.md content with YAML frontmatter including generatedBy
407
- // Use hyphen-based command references for tools where filename === command name (oh-my-pi, opencode, pi)
408
- const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
511
+ const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
409
512
  const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
410
513
  // Write the skill file
411
514
  await FileSystemUtils.writeFile(skillFile, skillContent);
412
515
  }
413
516
  }
414
- if (!shouldGenerateSkills) {
517
+ if (shouldRemoveSkillsForTool(tool.value, delivery)) {
415
518
  const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
416
519
  removedSkillCount += await this.removeSkillDirs(skillsDir);
417
520
  }
@@ -425,11 +528,16 @@ export class InitCommand {
425
528
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
426
529
  }
427
530
  }
531
+ }
532
+ else if (deliveryIncludesCommands) {
533
+ if (resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
534
+ skillsInvocableCommandSkips.push(tool.value);
535
+ }
428
536
  else {
429
537
  commandsSkipped.push(tool.value);
430
538
  }
431
539
  }
432
- if (!shouldGenerateCommands) {
540
+ if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
433
541
  removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
434
542
  }
435
543
  spinner.succeed(`Setup complete for ${tool.name}`);
@@ -450,6 +558,7 @@ export class InitCommand {
450
558
  refreshedTools,
451
559
  failedTools,
452
560
  commandsSkipped,
561
+ skillsInvocableCommandSkips,
453
562
  removedCommandCount,
454
563
  removedSkillCount,
455
564
  };
@@ -496,8 +605,12 @@ export class InitCommand {
496
605
  const delivery = globalConfig.delivery ?? 'both';
497
606
  const workflows = getProfileWorkflows(profile, globalConfig.workflows);
498
607
  const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', ');
499
- const skillCount = delivery !== 'commands' ? getSkillTemplates(workflows).length : 0;
500
- const commandCount = delivery !== 'skills' ? getCommandContents(workflows).length : 0;
608
+ const skillCount = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, delivery))
609
+ ? getSkillTemplates(workflows).length
610
+ : 0;
611
+ const commandCount = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, delivery))
612
+ ? getCommandContents(workflows).length
613
+ : 0;
501
614
  if (skillCount > 0 && commandCount > 0) {
502
615
  console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
503
616
  }
@@ -516,12 +629,22 @@ export class InitCommand {
516
629
  if (results.commandsSkipped.length > 0) {
517
630
  console.log(chalk.dim(`Commands skipped for: ${results.commandsSkipped.join(', ')} (no adapter)`));
518
631
  }
632
+ if (results.skillsInvocableCommandSkips.length > 0) {
633
+ console.log(chalk.dim(`Commands skipped for: ${results.skillsInvocableCommandSkips.join(', ')} (uses skills)`));
634
+ }
519
635
  if (results.removedCommandCount > 0) {
520
636
  console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`));
521
637
  }
522
638
  if (results.removedSkillCount > 0) {
523
639
  console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
524
640
  }
641
+ // Show manual setup notes for tools that need extra configuration
642
+ for (const tool of successfulTools) {
643
+ const setupNote = AI_TOOLS.find((t) => t.value === tool.value)?.setupNote;
644
+ if (setupNote) {
645
+ console.log(chalk.yellow(`Setup required for ${tool.name}: ${setupNote}`));
646
+ }
647
+ }
525
648
  // Config status
526
649
  if (configStatus === 'created') {
527
650
  console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
@@ -537,17 +660,74 @@ export class InitCommand {
537
660
  console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
538
661
  }
539
662
  // Getting started (task 7.6: show propose if in profile)
540
- const globalCfg = getGlobalConfig();
541
- const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
542
- const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
543
- console.log();
544
- if (activeWorkflows.includes('propose')) {
663
+ const activeWorkflows = this.getActiveWorkflows();
664
+ // When no tool got /opsx:* commands, point at the skill instead of a
665
+ // command that does not exist.
666
+ const activeDelivery = getGlobalConfig().delivery ?? 'both';
667
+ const commandsGenerated = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, activeDelivery));
668
+ const skillsGenerated = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, activeDelivery));
669
+ // Each hint line must be a usable instruction for the tool it serves.
670
+ // Tools that generated commands are told the command name their files
671
+ // answer to (/opsx:* when namespaced under opsx/, /opsx-* when the
672
+ // filename is the command); tools that only got skills are told their
673
+ // documented skill invocation (Kimi Code: /skill:openspec-*; Codex CLI:
674
+ // $openspec-*; others: /openspec-*). Tools that got no artifacts are
675
+ // covered by the configuration correction instead. When the selection
676
+ // disagrees, print one line per distinct instruction, labeled with the
677
+ // tools it applies to.
678
+ const startHintLines = (command) => {
679
+ const hintToTools = new Map();
680
+ for (const tool of successfulTools) {
681
+ let hint;
682
+ if (shouldGenerateCommandsForTool(tool.value, activeDelivery)) {
683
+ const transformer = getTransformerForTool(tool.value, activeDelivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
684
+ hint = `Start your first change: ${transformer ? transformer(command) : command} "your idea"`;
685
+ }
686
+ else if (shouldGenerateSkillsForTool(tool.value, activeDelivery)) {
687
+ hint = `Start your first change: ${getSkillReferenceTransformer(tool.value)(command)} "your idea"`;
688
+ }
689
+ else {
690
+ continue;
691
+ }
692
+ hintToTools.set(hint, [...(hintToTools.get(hint) ?? []), tool.name]);
693
+ }
694
+ if (hintToTools.size === 0) {
695
+ // No successful tools: keep the generic command hint
696
+ return [`Start your first change: ${command} "your idea"`];
697
+ }
698
+ if (hintToTools.size === 1) {
699
+ return [[...hintToTools.keys()][0]];
700
+ }
701
+ return [...hintToTools.entries()].map(([hint, toolNames]) => `${hint} (${toolNames.join(', ')})`);
702
+ };
703
+ const printStartHints = (command) => {
545
704
  console.log(chalk.bold('Getting started:'));
546
- console.log(' Start your first change: /opsx:propose "your idea"');
705
+ for (const line of startHintLines(command)) {
706
+ console.log(` ${line}`);
707
+ }
708
+ };
709
+ console.log();
710
+ // delivery=commands with tools that only support skills: those tools get
711
+ // no artifacts at all, so print a per-tool configuration correction
712
+ // rather than leave them with a dead (or missing) instruction — even
713
+ // when other selected tools did get commands or skills.
714
+ const zeroArtifactTools = successfulTools.filter((tool) => !shouldGenerateSkillsForTool(tool.value, activeDelivery) &&
715
+ !shouldGenerateCommandsForTool(tool.value, activeDelivery));
716
+ if (zeroArtifactTools.length > 0) {
717
+ const names = zeroArtifactTools.map((tool) => tool.name).join(', ');
718
+ console.log(chalk.yellow(`No skills or commands were generated for ${names}: delivery is set to 'commands' but ` +
719
+ `${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
720
+ `Run 'openspec config set delivery both' to generate skills.`));
721
+ }
722
+ if (successfulTools.length > 0 && !commandsGenerated && !skillsGenerated) {
723
+ // Nothing was generated for any tool: the correction above is the
724
+ // whole story, so don't advertise an invocation that doesn't exist.
725
+ }
726
+ else if (activeWorkflows.includes('propose')) {
727
+ printStartHints('/opsx:propose');
547
728
  }
548
729
  else if (activeWorkflows.includes('new')) {
549
- console.log(chalk.bold('Getting started:'));
550
- console.log(' Start your first change: /opsx:new "your idea"');
730
+ printStartHints('/opsx:new');
551
731
  }
552
732
  else {
553
733
  console.log("Done. Run 'openspec config profile' to configure your workflows.");
@@ -556,10 +736,16 @@ export class InitCommand {
556
736
  console.log();
557
737
  console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
558
738
  console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);
559
- // Restart instruction if any tools were configured
560
- if (results.createdTools.length > 0 || results.refreshedTools.length > 0) {
739
+ // Restart instruction if any tools were configured and got a surface
740
+ // (when nothing was generated there is nothing a restart would pick up);
741
+ // only mention commands when commands were actually generated. Not "slash
742
+ // commands": Amazon Q's generated files are prompt-library entries invoked
743
+ // with @, so a restart line promising slash commands would be wrong for it.
744
+ if ((results.createdTools.length > 0 || results.refreshedTools.length > 0) && (commandsGenerated || skillsGenerated)) {
561
745
  console.log();
562
- console.log(chalk.white('Restart your IDE for slash commands to take effect.'));
746
+ console.log(chalk.white(commandsGenerated
747
+ ? 'Restart your IDE for the new commands to take effect.'
748
+ : 'Restart your IDE for the new skills to take effect.'));
563
749
  }
564
750
  console.log();
565
751
  }
@@ -2,6 +2,7 @@
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 type { WorkflowId } from './profiles.js';
5
6
  /**
6
7
  * Legacy config file names from the old ToolRegistry.
7
8
  * These were config files created at project root with OpenSpec markers.
@@ -13,6 +14,11 @@ export declare const LEGACY_CONFIG_FILES: readonly ["CLAUDE.md", "CLINE.md", "CO
13
14
  * Some tools used a directory structure, others used individual files.
14
15
  */
15
16
  export declare const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashCommandPattern>;
17
+ /**
18
+ * Global legacy prompt locations that live outside the project tree and require
19
+ * allowlisted matching instead of broad glob-based cleanup.
20
+ */
21
+ export declare const LEGACY_GLOBAL_SLASH_COMMAND_PATHS: Record<string, LegacyGlobalPromptPattern>;
16
22
  /**
17
23
  * Pattern types for legacy slash commands
18
24
  */
@@ -21,6 +27,28 @@ export interface LegacySlashCommandPattern {
21
27
  path?: string;
22
28
  pattern?: string | string[];
23
29
  }
30
+ /**
31
+ * Describes a managed global prompt home and the exact filenames OpenSpec is
32
+ * allowed to treat as legacy artifacts there.
33
+ */
34
+ export interface LegacyGlobalPromptPattern {
35
+ managedFileNames: readonly string[];
36
+ workflowIdsByFileName?: Readonly<Record<string, readonly WorkflowId[]>>;
37
+ resolvePromptDir: () => string;
38
+ replacementLabel?: string;
39
+ }
40
+ /**
41
+ * Workflow-aware metadata for a detected global legacy prompt that is safe for
42
+ * replacement-gated cleanup.
43
+ */
44
+ export interface LegacyGlobalPromptMatch {
45
+ path: string;
46
+ toolId: string;
47
+ managedFileName: string;
48
+ workflowIds: readonly WorkflowId[];
49
+ replacementLabel?: string;
50
+ }
51
+ export declare function getCodexPromptDir(): string;
24
52
  /**
25
53
  * Result of legacy artifact detection
26
54
  */
@@ -33,6 +61,10 @@ export interface LegacyDetectionResult {
33
61
  slashCommandDirs: string[];
34
62
  /** Legacy slash command files found (for file-based tools) */
35
63
  slashCommandFiles: string[];
64
+ /** Managed global command/prompt files found outside the project root */
65
+ globalSlashCommandFiles: string[];
66
+ /** Details for managed global command/prompt files */
67
+ globalSlashCommandDetails?: LegacyGlobalPromptMatch[];
36
68
  /** Whether openspec/AGENTS.md exists */
37
69
  hasOpenspecAgents: boolean;
38
70
  /** Whether openspec/project.md exists (preserved, migration hint only) */
@@ -111,6 +143,8 @@ export declare function removeMarkerBlock(content: string): string;
111
143
  export interface CleanupResult {
112
144
  /** Files that were deleted entirely */
113
145
  deletedFiles: string[];
146
+ /** Replacement labels for deleted files when cleanup knows the new surface */
147
+ deletedFileReplacementLabels?: Record<string, string>;
114
148
  /** Files that had marker blocks removed */
115
149
  modifiedFiles: string[];
116
150
  /** Directories that were deleted */
@@ -144,6 +178,11 @@ export declare function formatCleanupSummary(result: CleanupResult): string;
144
178
  * @returns Formatted summary string showing what was found
145
179
  */
146
180
  export declare function formatDetectionSummary(detection: LegacyDetectionResult): string;
181
+ /**
182
+ * Generates a summary for managed global prompt files whose cleanup must wait
183
+ * until replacement skills are installed.
184
+ */
185
+ export declare function formatDeferredGlobalPromptSummary(detection: LegacyDetectionResult): string;
147
186
  /**
148
187
  * Extract tool IDs from detected legacy artifacts.
149
188
  * Uses LEGACY_SLASH_COMMAND_PATHS to map paths back to tool IDs.
@@ -152,6 +191,26 @@ export declare function formatDetectionSummary(detection: LegacyDetectionResult)
152
191
  * @returns Array of tool IDs that had legacy artifacts
153
192
  */
154
193
  export declare function getToolsFromLegacyArtifacts(detection: LegacyDetectionResult): string[];
194
+ /**
195
+ * Normalizes global Codex prompt matches so callers can rely on workflow-aware
196
+ * metadata even when older detection results only carry file paths.
197
+ */
198
+ export declare function getLegacyGlobalPromptMatches(detection: LegacyDetectionResult): LegacyGlobalPromptMatch[];
199
+ /**
200
+ * Collects workflow IDs inferred from detected legacy global prompts for a
201
+ * specific tool.
202
+ */
203
+ export declare function getLegacyWorkflowIdsForTool(detection: LegacyDetectionResult, toolId: string): WorkflowId[];
204
+ /**
205
+ * Returns a detection snapshot with global Codex prompt cleanup removed so
206
+ * callers can safely perform the immediate, non-deferred cleanup pass.
207
+ */
208
+ export declare function omitGlobalLegacyPromptFiles(detection: LegacyDetectionResult): LegacyDetectionResult;
209
+ /**
210
+ * Builds a detection snapshot containing only the selected global Codex prompt
211
+ * matches for replacement-gated cleanup.
212
+ */
213
+ export declare function pickGlobalLegacyPromptFiles(detection: LegacyDetectionResult, filePaths: readonly string[]): LegacyDetectionResult;
155
214
  /**
156
215
  * Generates a migration hint message for project.md.
157
216
  * This is shown when project.md exists and needs manual migration to config.yaml.