@codewalla_india/openspec 1.3.1 → 1.3.3

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 (333) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +214 -133
  3. package/bin/openspec.js +5 -5
  4. package/dist/cli/index.js +99 -62
  5. package/dist/commands/change.d.ts +0 -2
  6. package/dist/commands/change.js +107 -86
  7. package/dist/commands/config.js +14 -16
  8. package/dist/commands/doctor.js +9 -4
  9. package/dist/commands/feedback.js +79 -30
  10. package/dist/commands/modify.d.ts +26 -0
  11. package/dist/commands/modify.js +147 -0
  12. package/dist/commands/schema.js +170 -91
  13. package/dist/commands/show.js +5 -2
  14. package/dist/commands/spec.js +49 -29
  15. package/dist/commands/store.js +15 -21
  16. package/dist/commands/validate.d.ts +8 -0
  17. package/dist/commands/validate.js +43 -20
  18. package/dist/commands/workflow/index.d.ts +2 -2
  19. package/dist/commands/workflow/index.js +1 -1
  20. package/dist/commands/workflow/instructions.d.ts +6 -26
  21. package/dist/commands/workflow/instructions.js +183 -376
  22. package/dist/commands/workflow/new-change.d.ts +0 -4
  23. package/dist/commands/workflow/new-change.js +10 -28
  24. package/dist/commands/workflow/shared.d.ts +34 -19
  25. package/dist/commands/workflow/shared.js +34 -4
  26. package/dist/commands/workflow/status.js +10 -32
  27. package/dist/commands/workflow/templates.js +15 -5
  28. package/dist/commands/workset.d.ts +2 -2
  29. package/dist/commands/workset.js +19 -22
  30. package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
  31. package/dist/comprehension-quiz/fingerprint.js +84 -0
  32. package/dist/comprehension-quiz/index.d.ts +42 -0
  33. package/dist/comprehension-quiz/index.js +47 -0
  34. package/dist/comprehension-quiz/pass-record.d.ts +44 -0
  35. package/dist/comprehension-quiz/pass-record.js +98 -0
  36. package/dist/comprehension-quiz/providers/anthropic.d.ts +35 -0
  37. package/dist/comprehension-quiz/providers/anthropic.js +69 -0
  38. package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
  39. package/dist/comprehension-quiz/providers/factory.js +174 -0
  40. package/dist/comprehension-quiz/providers/interface.d.ts +98 -0
  41. package/dist/comprehension-quiz/providers/interface.js +150 -0
  42. package/dist/comprehension-quiz/providers/local.d.ts +35 -0
  43. package/dist/comprehension-quiz/providers/local.js +69 -0
  44. package/dist/comprehension-quiz/providers/ollama.d.ts +35 -0
  45. package/dist/comprehension-quiz/providers/ollama.js +69 -0
  46. package/dist/comprehension-quiz/providers/openai.d.ts +35 -0
  47. package/dist/comprehension-quiz/providers/openai.js +72 -0
  48. package/dist/comprehension-quiz/question-generator.d.ts +117 -0
  49. package/dist/comprehension-quiz/question-generator.js +203 -0
  50. package/dist/comprehension-quiz/quiz-executor.d.ts +75 -0
  51. package/dist/comprehension-quiz/quiz-executor.js +181 -0
  52. package/dist/comprehension-quiz/types.d.ts +137 -0
  53. package/dist/comprehension-quiz/types.js +11 -0
  54. package/dist/core/archive.d.ts +14 -1
  55. package/dist/core/archive.js +1350 -190
  56. package/dist/core/artifact-graph/graph.d.ts +28 -9
  57. package/dist/core/artifact-graph/graph.js +94 -43
  58. package/dist/core/artifact-graph/index.d.ts +1 -1
  59. package/dist/core/artifact-graph/index.js +1 -1
  60. package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
  61. package/dist/core/artifact-graph/instruction-loader.js +70 -12
  62. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  63. package/dist/core/artifact-graph/outputs.js +67 -5
  64. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  65. package/dist/core/artifact-graph/resolver.js +69 -15
  66. package/dist/core/artifact-graph/types.js +21 -3
  67. package/dist/core/available-tools.d.ts +2 -2
  68. package/dist/core/available-tools.js +14 -3
  69. package/dist/core/change-metadata/schema.d.ts +2 -0
  70. package/dist/core/change-metadata/schema.js +13 -0
  71. package/dist/core/change-status-policy.d.ts +10 -1
  72. package/dist/core/change-status-policy.js +30 -2
  73. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  74. package/dist/core/command-generation/adapters/amazon-q.js +11 -5
  75. package/dist/core/command-generation/adapters/antigravity.js +6 -5
  76. package/dist/core/command-generation/adapters/auggie.js +7 -6
  77. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  78. package/dist/core/command-generation/adapters/bob.js +11 -10
  79. package/dist/core/command-generation/adapters/claude.d.ts +1 -1
  80. package/dist/core/command-generation/adapters/claude.js +12 -17
  81. package/dist/core/command-generation/adapters/cline.js +5 -5
  82. package/dist/core/command-generation/adapters/codebuddy.js +8 -7
  83. package/dist/core/command-generation/adapters/continue.js +8 -7
  84. package/dist/core/command-generation/adapters/costrict.js +7 -6
  85. package/dist/core/command-generation/adapters/crush.js +9 -9
  86. package/dist/core/command-generation/adapters/cursor.js +8 -8
  87. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  88. package/dist/core/command-generation/adapters/devin.js +36 -0
  89. package/dist/core/command-generation/adapters/factory.js +7 -6
  90. package/dist/core/command-generation/adapters/gemini.js +40 -5
  91. package/dist/core/command-generation/adapters/github-copilot.js +6 -5
  92. package/dist/core/command-generation/adapters/iflow.js +9 -8
  93. package/dist/core/command-generation/adapters/index.d.ts +4 -2
  94. package/dist/core/command-generation/adapters/index.js +4 -2
  95. package/dist/core/command-generation/adapters/junie.js +6 -5
  96. package/dist/core/command-generation/adapters/kilocode.js +1 -1
  97. package/dist/core/command-generation/adapters/kiro.js +6 -5
  98. package/dist/core/command-generation/adapters/lingma.js +9 -9
  99. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  100. package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
  101. package/dist/core/command-generation/adapters/opencode.js +6 -8
  102. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  103. package/dist/core/command-generation/adapters/pi.js +7 -10
  104. package/dist/core/command-generation/adapters/qoder.js +9 -9
  105. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  106. package/dist/core/command-generation/adapters/qwen.js +14 -9
  107. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  108. package/dist/core/command-generation/adapters/roocode.js +9 -9
  109. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  110. package/dist/core/command-generation/adapters/trae.js +28 -0
  111. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  112. package/dist/core/command-generation/adapters/zcode.js +33 -0
  113. package/dist/core/command-generation/generator.d.ts +8 -0
  114. package/dist/core/command-generation/generator.js +15 -1
  115. package/dist/core/command-generation/index.d.ts +1 -1
  116. package/dist/core/command-generation/index.js +1 -1
  117. package/dist/core/command-generation/invocation.d.ts +73 -0
  118. package/dist/core/command-generation/invocation.js +77 -0
  119. package/dist/core/command-generation/registry.js +8 -4
  120. package/dist/core/command-generation/types.d.ts +9 -2
  121. package/dist/core/command-generation/yaml.d.ts +20 -6
  122. package/dist/core/command-generation/yaml.js +31 -21
  123. package/dist/core/command-surface.d.ts +15 -0
  124. package/dist/core/command-surface.js +33 -0
  125. package/dist/core/completions/command-registry.js +16 -61
  126. package/dist/core/completions/generators/bash-generator.js +41 -41
  127. package/dist/core/completions/generators/fish-generator.js +7 -7
  128. package/dist/core/completions/generators/powershell-generator.js +44 -37
  129. package/dist/core/completions/generators/zsh-generator.js +33 -33
  130. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  131. package/dist/core/completions/installers/zsh-installer.js +20 -4
  132. package/dist/core/completions/templates/bash-templates.js +24 -24
  133. package/dist/core/completions/templates/fish-templates.js +38 -38
  134. package/dist/core/completions/templates/powershell-templates.js +28 -28
  135. package/dist/core/completions/templates/zsh-templates.js +39 -39
  136. package/dist/core/config-prompts.d.ts +1 -1
  137. package/dist/core/config-prompts.js +14 -1
  138. package/dist/core/config-schema.d.ts +10 -1
  139. package/dist/core/config-schema.js +67 -1
  140. package/dist/core/config.d.ts +15 -0
  141. package/dist/core/config.js +46 -5
  142. package/dist/core/file-state.js +56 -24
  143. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  144. package/dist/core/github-copilot/cloud-agent.js +497 -0
  145. package/dist/core/global-config.d.ts +16 -0
  146. package/dist/core/init.d.ts +58 -0
  147. package/dist/core/init.js +474 -73
  148. package/dist/core/legacy-cleanup.d.ts +59 -0
  149. package/dist/core/legacy-cleanup.js +253 -24
  150. package/dist/core/list.js +23 -16
  151. package/dist/core/migration.d.ts +73 -1
  152. package/dist/core/migration.js +360 -11
  153. package/dist/core/modify/conflict-detector.d.ts +26 -0
  154. package/dist/core/modify/conflict-detector.js +62 -0
  155. package/dist/core/modify/dependency-propagator.d.ts +38 -0
  156. package/dist/core/modify/dependency-propagator.js +103 -0
  157. package/dist/core/modify/history-tracker.d.ts +34 -0
  158. package/dist/core/modify/history-tracker.js +114 -0
  159. package/dist/core/modify/index.d.ts +7 -0
  160. package/dist/core/modify/index.js +7 -0
  161. package/dist/core/onboarding-commands.d.ts +30 -0
  162. package/dist/core/onboarding-commands.js +38 -0
  163. package/dist/core/openspec-root.js +18 -15
  164. package/dist/core/parsers/change-parser.d.ts +17 -2
  165. package/dist/core/parsers/change-parser.js +35 -21
  166. package/dist/core/parsers/code-fence.d.ts +15 -0
  167. package/dist/core/parsers/code-fence.js +49 -0
  168. package/dist/core/parsers/markdown-parser.d.ts +0 -3
  169. package/dist/core/parsers/markdown-parser.js +7 -60
  170. package/dist/core/parsers/requirement-blocks.d.ts +29 -0
  171. package/dist/core/parsers/requirement-blocks.js +150 -31
  172. package/dist/core/parsers/requirement-text.d.ts +48 -0
  173. package/dist/core/parsers/requirement-text.js +98 -0
  174. package/dist/core/parsers/spec-structure.d.ts +1 -1
  175. package/dist/core/parsers/spec-structure.js +20 -30
  176. package/dist/core/profile-sync-drift.d.ts +0 -8
  177. package/dist/core/profile-sync-drift.js +49 -50
  178. package/dist/core/profiles.d.ts +2 -2
  179. package/dist/core/profiles.js +2 -2
  180. package/dist/core/project-config.d.ts +27 -18
  181. package/dist/core/project-config.js +101 -34
  182. package/dist/core/references.js +42 -4
  183. package/dist/core/relationship-health.d.ts +8 -0
  184. package/dist/core/relationship-health.js +13 -0
  185. package/dist/core/root-selection.d.ts +9 -6
  186. package/dist/core/root-selection.js +42 -5
  187. package/dist/core/schemas/base.schema.js +7 -2
  188. package/dist/core/shared/allowed-tools.d.ts +12 -0
  189. package/dist/core/shared/allowed-tools.js +12 -0
  190. package/dist/core/shared/index.d.ts +1 -0
  191. package/dist/core/shared/index.js +1 -0
  192. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  193. package/dist/core/shared/skill-content-equivalence.js +41 -0
  194. package/dist/core/shared/skill-generation.js +17 -13
  195. package/dist/core/shared/skill-paths.d.ts +13 -0
  196. package/dist/core/shared/skill-paths.js +23 -0
  197. package/dist/core/shared/tool-detection.d.ts +27 -7
  198. package/dist/core/shared/tool-detection.js +167 -37
  199. package/dist/core/shared-skill-target.d.ts +16 -0
  200. package/dist/core/shared-skill-target.js +156 -0
  201. package/dist/core/specs-apply.d.ts +80 -33
  202. package/dist/core/specs-apply.js +667 -145
  203. package/dist/core/store/git.d.ts +13 -0
  204. package/dist/core/store/git.js +24 -0
  205. package/dist/core/store/operations.js +20 -0
  206. package/dist/core/templates/skill-templates.d.ts +3 -2
  207. package/dist/core/templates/skill-templates.js +3 -2
  208. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  209. package/dist/core/templates/workflows/apply-change.js +186 -224
  210. package/dist/core/templates/workflows/archive-change.js +387 -260
  211. package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
  212. package/dist/core/templates/workflows/continue-change.js +210 -219
  213. package/dist/core/templates/workflows/explore.js +479 -443
  214. package/dist/core/templates/workflows/feedback.js +97 -97
  215. package/dist/core/templates/workflows/ff-change.js +200 -191
  216. package/dist/core/templates/workflows/modify-change.d.ts +4 -1
  217. package/dist/core/templates/workflows/modify-change.js +193 -71
  218. package/dist/core/templates/workflows/new-change.js +127 -134
  219. package/dist/core/templates/workflows/onboard.js +548 -542
  220. package/dist/core/templates/workflows/propose.js +273 -217
  221. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  222. package/dist/core/templates/workflows/store-selection.js +1 -1
  223. package/dist/core/templates/workflows/sync-specs.js +500 -273
  224. package/dist/core/templates/workflows/update-change.d.ts +10 -0
  225. package/dist/core/templates/workflows/update-change.js +177 -0
  226. package/dist/core/templates/workflows/verify-change.js +324 -326
  227. package/dist/core/update.d.ts +40 -5
  228. package/dist/core/update.js +411 -96
  229. package/dist/core/validation/constants.d.ts +4 -1
  230. package/dist/core/validation/constants.js +4 -1
  231. package/dist/core/validation/plan-validator.d.ts +8 -0
  232. package/dist/core/validation/plan-validator.js +45 -0
  233. package/dist/core/validation/task-numbering.d.ts +16 -0
  234. package/dist/core/validation/task-numbering.js +57 -0
  235. package/dist/core/validation/validator.d.ts +27 -3
  236. package/dist/core/validation/validator.js +337 -48
  237. package/dist/core/version-check.d.ts +152 -0
  238. package/dist/core/version-check.js +731 -0
  239. package/dist/core/view.js +13 -18
  240. package/dist/prompts/searchable-multi-select.js +1 -1
  241. package/dist/telemetry/caller-detection.d.ts +27 -0
  242. package/dist/telemetry/caller-detection.js +85 -0
  243. package/dist/telemetry/caller-detection.test.d.ts +5 -0
  244. package/dist/telemetry/config.d.ts +48 -2
  245. package/dist/telemetry/config.js +113 -11
  246. package/dist/telemetry/config.test.d.ts +5 -0
  247. package/dist/telemetry/deduplication.d.ts +35 -0
  248. package/dist/telemetry/deduplication.js +82 -0
  249. package/dist/telemetry/deduplication.test.d.ts +5 -0
  250. package/dist/telemetry/identity.d.ts +62 -22
  251. package/dist/telemetry/identity.js +147 -102
  252. package/dist/telemetry/index.d.ts +126 -15
  253. package/dist/telemetry/index.js +569 -37
  254. package/dist/telemetry/sanitization.d.ts +40 -0
  255. package/dist/telemetry/sanitization.js +114 -0
  256. package/dist/telemetry/sanitization.test.d.ts +5 -0
  257. package/dist/telemetry/throttling.d.ts +20 -0
  258. package/dist/telemetry/throttling.js +70 -0
  259. package/dist/ui/welcome-screen.d.ts +9 -1
  260. package/dist/ui/welcome-screen.js +91 -38
  261. package/dist/utils/change-metadata.d.ts +43 -0
  262. package/dist/utils/change-metadata.js +123 -7
  263. package/dist/utils/change-utils.d.ts +10 -4
  264. package/dist/utils/change-utils.js +20 -13
  265. package/dist/utils/ci.d.ts +12 -0
  266. package/dist/utils/ci.js +16 -0
  267. package/dist/utils/command-references.d.ts +92 -6
  268. package/dist/utils/command-references.js +180 -7
  269. package/dist/utils/date.d.ts +8 -0
  270. package/dist/utils/date.js +13 -0
  271. package/dist/utils/file-system.d.ts +10 -0
  272. package/dist/utils/file-system.js +70 -0
  273. package/dist/utils/index.d.ts +1 -1
  274. package/dist/utils/index.js +1 -1
  275. package/dist/utils/interactive.d.ts +23 -0
  276. package/dist/utils/interactive.js +32 -0
  277. package/dist/utils/item-discovery.d.ts +19 -0
  278. package/dist/utils/item-discovery.js +30 -48
  279. package/dist/utils/shell-detection.d.ts +1 -1
  280. package/dist/utils/shell-detection.js +64 -11
  281. package/dist/utils/spec-discovery.d.ts +40 -0
  282. package/dist/utils/spec-discovery.js +115 -0
  283. package/dist/utils/task-progress.d.ts +29 -1
  284. package/dist/utils/task-progress.js +109 -15
  285. package/package.json +96 -84
  286. package/schemas/spec-driven/schema.yaml +252 -184
  287. package/schemas/spec-driven/templates/design.md +19 -19
  288. package/schemas/spec-driven/templates/plan-template.md +91 -0
  289. package/schemas/spec-driven/templates/proposal.md +29 -24
  290. package/schemas/spec-driven/templates/spec.md +11 -8
  291. package/schemas/spec-driven/templates/tasks.md +9 -9
  292. package/scripts/postinstall.js +83 -83
  293. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  294. package/dist/core/command-generation/adapters/codex.js +0 -39
  295. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  296. package/dist/core/command-generation/adapters/windsurf.js +0 -38
  297. package/dist/core/comprehension/config.d.ts +0 -20
  298. package/dist/core/comprehension/config.js +0 -23
  299. package/dist/core/comprehension/fingerprint.d.ts +0 -14
  300. package/dist/core/comprehension/fingerprint.js +0 -33
  301. package/dist/core/comprehension/index.d.ts +0 -57
  302. package/dist/core/comprehension/index.js +0 -87
  303. package/dist/core/comprehension/pass-record.d.ts +0 -29
  304. package/dist/core/comprehension/pass-record.js +0 -64
  305. package/dist/core/comprehension/stats.d.ts +0 -35
  306. package/dist/core/comprehension/stats.js +0 -133
  307. package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
  308. package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
  309. package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
  310. package/dist/core/templates/workflows/mcp-guidance.js +0 -131
  311. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
  312. package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
  313. package/dist/telemetry/caller.d.ts +0 -5
  314. package/dist/telemetry/caller.js +0 -29
  315. package/dist/telemetry/client.d.ts +0 -27
  316. package/dist/telemetry/client.js +0 -127
  317. package/dist/telemetry/command-context.d.ts +0 -13
  318. package/dist/telemetry/command-context.js +0 -59
  319. package/dist/telemetry/comprehension.d.ts +0 -44
  320. package/dist/telemetry/comprehension.js +0 -105
  321. package/dist/telemetry/content.d.ts +0 -10
  322. package/dist/telemetry/content.js +0 -56
  323. package/dist/telemetry/git-stats.d.ts +0 -12
  324. package/dist/telemetry/git-stats.js +0 -69
  325. package/dist/telemetry/identify-cache.d.ts +0 -7
  326. package/dist/telemetry/identify-cache.js +0 -47
  327. package/dist/telemetry/input.d.ts +0 -17
  328. package/dist/telemetry/input.js +0 -68
  329. package/dist/telemetry/marker.d.ts +0 -37
  330. package/dist/telemetry/marker.js +0 -67
  331. package/dist/telemetry/workflow.d.ts +0 -85
  332. package/dist/telemetry/workflow.js +0 -318
  333. package/schemas/spec-driven/templates/plan.md +0 -19
package/dist/core/init.js CHANGED
@@ -12,19 +12,22 @@ 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, usesNaturalLanguageSkillReferences } 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';
22
- import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
21
+ import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDeferredGlobalPromptSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
22
+ import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } 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';
27
- import { setupTelemetryIdentity } from '../telemetry/index.js';
26
+ import { writeSharedSkillTarget } from './shared-skill-target.js';
27
+ import { migrateIfNeeded, migrateLegacyToolDirs, describeLegacyMigration, keptInPlaceNotice, hasMovableContent, scanInstalledWorkflows as scanInstalledWorkflowsShared } from './migration.js';
28
+ import { readIdentity, writeIdentity, promptForIdentity, validateIdentity, normalizeIdentity } from '../telemetry/identity.js';
29
+ import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
30
+ import { writeCopilotCloudFiles, readCopilotCloudOptIn, hasExistingManagedCloudFiles, persistCopilotCloudOptIn, removeCopilotCloudFiles, findUnmanagedCloudFiles, listManagedCloudFiles, } from './github-copilot/cloud-agent.js';
28
31
  const require = createRequire(import.meta.url);
29
32
  const { version: OPENSPEC_VERSION } = require('../../package.json');
30
33
  // -----------------------------------------------------------------------------
@@ -40,6 +43,7 @@ const WORKFLOW_TO_SKILL_DIR = {
40
43
  'new': 'openspec-new-change',
41
44
  'continue': 'openspec-continue-change',
42
45
  'apply': 'openspec-apply-change',
46
+ 'update': 'openspec-update-change',
43
47
  'ff': 'openspec-ff-change',
44
48
  'sync': 'openspec-sync-specs',
45
49
  'archive': 'openspec-archive-change',
@@ -56,11 +60,15 @@ export class InitCommand {
56
60
  force;
57
61
  interactiveOption;
58
62
  profileOverride;
63
+ animation;
64
+ copilotCloudOption;
59
65
  constructor(options = {}) {
60
66
  this.toolsArg = options.tools;
61
67
  this.force = options.force ?? false;
62
68
  this.interactiveOption = options.interactive;
63
69
  this.profileOverride = options.profile;
70
+ this.animation = options.animation ?? true;
71
+ this.copilotCloudOption = options.copilotCloud;
64
72
  }
65
73
  async execute(targetPath) {
66
74
  const projectPath = path.resolve(targetPath);
@@ -91,37 +99,124 @@ export class InitCommand {
91
99
  }
92
100
  }
93
101
  // Check for legacy artifacts and handle cleanup
94
- await this.handleLegacyCleanup(projectPath, extendMode);
102
+ const deferredLegacyCleanup = await this.handleLegacyCleanup(projectPath, extendMode);
103
+ // Migrate OpenSpec-managed skills left in renamed tool directories
104
+ // (e.g. .kimi -> .kimi-code) before detection so they stay recognized.
105
+ migrateLegacyToolDirs(projectPath);
95
106
  // Detect available tools in the project (task 7.1)
96
107
  const detectedTools = getAvailableTools(projectPath);
97
108
  // Migration check: migrate existing projects to profile system (task 7.3)
98
109
  if (extendMode) {
99
110
  migrateIfNeeded(projectPath, detectedTools);
100
111
  }
112
+ // Validate profile override early so invalid values fail before tool setup.
113
+ // The resolved value is consumed later when generation reads effective config.
114
+ // This runs ahead of the welcome screen so an invalid --profile does not make
115
+ // the user press Enter before seeing the error.
116
+ this.resolveProfileOverride();
101
117
  // Show animated welcome screen (interactive mode only)
102
118
  const canPrompt = this.canPromptInteractively();
103
119
  if (canPrompt) {
104
120
  const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
105
- await showWelcomeScreen();
121
+ await showWelcomeScreen(this.getActiveWorkflows(), { animate: this.animation });
122
+ }
123
+ // Prompt for identity if not already configured (task 7.1)
124
+ const existingIdentity = await readIdentity();
125
+ if (!existingIdentity) {
126
+ const identity = await promptForIdentity();
127
+ if (identity) {
128
+ const normalized = normalizeIdentity(identity);
129
+ const validation = validateIdentity(normalized);
130
+ if (validation.valid) {
131
+ await writeIdentity(normalized);
132
+ }
133
+ else {
134
+ console.error(`Invalid identity: ${validation.errors.join(', ')}`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+ else {
139
+ console.error('Identity is required for OpenSpec. Please provide an email or username.');
140
+ process.exit(1);
141
+ }
106
142
  }
107
- await setupTelemetryIdentity({ interactive: canPrompt });
108
- // Validate profile override early so invalid values fail before tool setup.
109
- // The resolved value is consumed later when generation reads effective config.
110
- this.resolveProfileOverride();
111
143
  // Get tool states before processing
112
144
  const toolStates = getToolStates(projectPath);
113
145
  // Get tool selection (pass detected tools for pre-selection)
114
146
  const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
115
147
  // Validate selected tools
116
- const validatedTools = this.validateTools(selectedToolIds, toolStates);
148
+ const validatedTools = this.validateTools(selectedToolIds, toolStates, projectPath);
149
+ // Selecting a renamed tool is consent to leave its former directory:
150
+ // init is about to write the current one, and leaving OpenSpec content
151
+ // behind would give the user two installs of the same tool.
152
+ for (const migration of migrateLegacyToolDirs(projectPath, validatedTools.map((tool) => tool.value))) {
153
+ if (hasMovableContent(migration)) {
154
+ console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
155
+ }
156
+ const kept = keptInPlaceNotice(migration);
157
+ if (kept)
158
+ console.log(chalk.dim(kept));
159
+ }
160
+ // Decide whether to generate GitHub Copilot cloud files. This is opt-in
161
+ // (see cloud-agent.ts): selecting the Copilot tool no longer silently
162
+ // writes a GitHub Actions workflow into the user's .github/. The decision
163
+ // is made before generation so the write can be gated, and persisted after
164
+ // config.yaml exists so future non-interactive updates honor it.
165
+ const copilotDecision = await this.resolveCopilotCloudDecision(projectPath, validatedTools);
117
166
  // Create directory structure and config
118
167
  await this.createDirectoryStructure(openspecPath, extendMode);
119
168
  // Generate skills and commands for each tool
120
- const results = await this.generateSkillsAndCommands(projectPath, validatedTools);
169
+ const results = await this.generateSkillsAndCommands(projectPath, validatedTools, copilotDecision.write);
170
+ // Legacy cleanup was deferred to avoid interfering with skill/command generation;
171
+ // now that outputs are written, finalize the cleanup (e.g. remove stale files).
172
+ if (deferredLegacyCleanup) {
173
+ await this.finalizeDeferredLegacyCleanup(projectPath, deferredLegacyCleanup);
174
+ }
121
175
  // Create config.yaml if needed
122
176
  const configStatus = await this.createConfig(openspecPath, extendMode);
177
+ // Persist an explicit Copilot cloud decision so `openspec update` (which
178
+ // never prompts) honors it. Best-effort: a config-write failure must not
179
+ // fail an otherwise-successful init.
180
+ if (copilotDecision.persist !== undefined) {
181
+ try {
182
+ await persistCopilotCloudOptIn(projectPath, copilotDecision.persist);
183
+ }
184
+ catch {
185
+ // Non-fatal: the files (if any) were still written correctly.
186
+ }
187
+ }
188
+ // An explicit opt-out means "no cloud files here": clean up any that a
189
+ // previous run (or an older OpenSpec) generated. Only OpenSpec-managed
190
+ // files are removed — a user-customized file is preserved.
191
+ let copilotRemoved = 0;
192
+ if (copilotDecision.optedOut) {
193
+ try {
194
+ copilotRemoved = await removeCopilotCloudFiles(projectPath);
195
+ }
196
+ catch {
197
+ // Non-fatal: removal targets files from a prior run; a failure here
198
+ // just leaves them for the next `openspec update` to clean up.
199
+ }
200
+ }
201
+ // Report the cloud outcome from what is actually on disk after the write,
202
+ // not from the decision alone: writing over a user-owned file is a no-op,
203
+ // and the alternate-agent path can remove a managed file — so list only
204
+ // managed files that exist, and separately flag any left-untouched ones.
205
+ const copilotSucceeded = [...results.createdTools, ...results.refreshedTools].some((tool) => tool.value === 'github-copilot');
206
+ const wroteCloud = copilotDecision.write && copilotSucceeded;
207
+ const copilotPresent = wroteCloud ? await listManagedCloudFiles(projectPath) : [];
208
+ const copilotCollisions = wroteCloud ? await findUnmanagedCloudFiles(projectPath) : [];
123
209
  // Display success message
124
- this.displaySuccessMessage(projectPath, validatedTools, results, configStatus);
210
+ this.displaySuccessMessage(projectPath, validatedTools, results, configStatus, {
211
+ write: copilotDecision.write,
212
+ skippedUndecided: copilotDecision.skippedUndecided,
213
+ present: copilotPresent,
214
+ collisions: copilotCollisions,
215
+ removed: copilotRemoved,
216
+ });
217
+ if (results.failedTools.length > 0) {
218
+ throw new Error(`OpenSpec setup failed for: ${results.failedTools.map((tool) => tool.name).join(', ')}`);
219
+ }
125
220
  }
126
221
  // ═══════════════════════════════════════════════════════════
127
222
  // VALIDATION & SETUP
@@ -141,6 +236,59 @@ export class InitCommand {
141
236
  return false;
142
237
  return isInteractive({ interactive: this.interactiveOption });
143
238
  }
239
+ /**
240
+ * Decide whether to generate GitHub Copilot cloud files, and whether to
241
+ * persist that decision. Precedence:
242
+ * 1. `--copilot-cloud` / `--no-copilot-cloud` flag (explicit this run)
243
+ * 2. persisted opt-in in config.yaml
244
+ * 3. managed files already present (migration for pre-opt-in projects)
245
+ * 4. interactive confirm (default No)
246
+ * 5. non-interactive with no signal: skip, and don't persist a default
247
+ *
248
+ * @returns `write` — generate the files this run; `persist` — value to write
249
+ * back to config (undefined = leave config untouched); `optedOut` — the user
250
+ * explicitly declined, so any already-generated managed files should be
251
+ * removed; `skippedUndecided` — selected but no signal and couldn't ask, so
252
+ * the caller can hint that the opt-in exists.
253
+ */
254
+ async resolveCopilotCloudDecision(projectPath, tools) {
255
+ const copilotSelected = tools.some((tool) => tool.value === 'github-copilot');
256
+ if (!copilotSelected) {
257
+ // A flag that can't apply is a likely mistake — say so rather than no-op.
258
+ if (this.copilotCloudOption !== undefined) {
259
+ console.log(chalk.yellow('--copilot-cloud/--no-copilot-cloud was ignored because the github-copilot tool was not selected.'));
260
+ }
261
+ return { write: false, optedOut: false, skippedUndecided: false };
262
+ }
263
+ if (this.copilotCloudOption !== undefined) {
264
+ return {
265
+ write: this.copilotCloudOption,
266
+ persist: this.copilotCloudOption,
267
+ optedOut: !this.copilotCloudOption,
268
+ skippedUndecided: false,
269
+ };
270
+ }
271
+ const persistedOptIn = readCopilotCloudOptIn(projectPath);
272
+ if (typeof persistedOptIn === 'boolean') {
273
+ return { write: persistedOptIn, optedOut: !persistedOptIn, skippedUndecided: false };
274
+ }
275
+ if (await hasExistingManagedCloudFiles(projectPath)) {
276
+ return { write: true, optedOut: false, skippedUndecided: false };
277
+ }
278
+ if (this.canPromptInteractively()) {
279
+ const { confirm } = await import('@inquirer/prompts');
280
+ const answer = await confirm({
281
+ message: 'Set up GitHub Copilot cloud coding-agent files? This is for the GitHub-hosted ' +
282
+ 'Copilot coding agent (github.com), not Copilot in your editor. It writes two files: ' +
283
+ '.github/workflows/copilot-setup-steps.yml and .github/agents/openspec.agent.md.',
284
+ default: false,
285
+ });
286
+ return { write: answer, persist: answer, optedOut: !answer, skippedUndecided: false };
287
+ }
288
+ // Non-interactive with no explicit signal: don't write, and leave the
289
+ // decision unpersisted so a later interactive run can still prompt.
290
+ return { write: false, optedOut: false, skippedUndecided: true };
291
+ }
144
292
  resolveProfileOverride() {
145
293
  if (this.profileOverride === undefined) {
146
294
  return undefined;
@@ -150,26 +298,50 @@ export class InitCommand {
150
298
  }
151
299
  throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`);
152
300
  }
301
+ /**
302
+ * Resolves the workflows the effective profile installs, so onboarding output
303
+ * only mentions commands that will actually exist.
304
+ */
305
+ getActiveWorkflows() {
306
+ const globalCfg = getGlobalConfig();
307
+ const activeProfile = this.resolveProfileOverride() ?? globalCfg.profile ?? 'core';
308
+ return [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
309
+ }
153
310
  // ═══════════════════════════════════════════════════════════
154
311
  // LEGACY CLEANUP
155
312
  // ═══════════════════════════════════════════════════════════
313
+ /**
314
+ * Cleans repo-local legacy artifacts immediately and defers global Codex prompt
315
+ * cleanup until replacement skills have been installed.
316
+ */
156
317
  async handleLegacyCleanup(projectPath, extendMode) {
157
318
  // Detect legacy artifacts
158
319
  const detection = await detectLegacyArtifacts(projectPath);
159
320
  if (!detection.hasLegacyArtifacts) {
160
- return; // No legacy artifacts found
321
+ return null; // No legacy artifacts found
161
322
  }
323
+ const immediateDetection = omitGlobalLegacyPromptFiles(detection);
162
324
  // Show what was detected
163
- console.log();
164
- console.log(formatDetectionSummary(detection));
165
- console.log();
325
+ const immediateSummary = formatDetectionSummary(immediateDetection);
326
+ if (immediateSummary) {
327
+ console.log();
328
+ console.log(immediateSummary);
329
+ console.log();
330
+ }
331
+ // Show which global prompts are deferred — they'll only be removed once
332
+ // the corresponding replacement skills are installed during generation.
333
+ const deferredSummary = formatDeferredGlobalPromptSummary(detection);
334
+ if (deferredSummary) {
335
+ console.log(deferredSummary);
336
+ console.log();
337
+ }
166
338
  const canPrompt = this.canPromptInteractively();
167
339
  if (this.force || !canPrompt) {
168
340
  // --force flag or non-interactive mode: proceed with cleanup automatically.
169
341
  // Legacy slash commands are 100% OpenSpec-managed, and config file cleanup
170
342
  // only removes markers (never deletes files), so auto-cleanup is safe.
171
- await this.performLegacyCleanup(projectPath, detection);
172
- return;
343
+ await this.performImmediateLegacyCleanup(projectPath, detection);
344
+ return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
173
345
  }
174
346
  // Interactive mode: prompt for confirmation
175
347
  const { confirm } = await import('@inquirer/prompts');
@@ -182,7 +354,51 @@ export class InitCommand {
182
354
  console.log(chalk.dim('Run with --force to skip this prompt, or manually remove legacy files.'));
183
355
  process.exit(0);
184
356
  }
185
- await this.performLegacyCleanup(projectPath, detection);
357
+ await this.performImmediateLegacyCleanup(projectPath, detection);
358
+ return detection.globalSlashCommandFiles.length > 0 ? { detection } : null;
359
+ }
360
+ /**
361
+ * Applies the safe subset of legacy cleanup that does not depend on newly
362
+ * generated Codex skills.
363
+ */
364
+ async performImmediateLegacyCleanup(projectPath, detection) {
365
+ const immediateDetection = omitGlobalLegacyPromptFiles(detection);
366
+ if (!immediateDetection.hasLegacyArtifacts) {
367
+ return;
368
+ }
369
+ await this.performLegacyCleanup(projectPath, immediateDetection);
370
+ }
371
+ /**
372
+ * Removes only the legacy global Codex prompts whose workflows now have
373
+ * replacement skills in the project.
374
+ */
375
+ async finalizeDeferredLegacyCleanup(projectPath, deferredCleanup) {
376
+ const availableCodexWorkflows = await this.getInstalledWorkflowsForTool(projectPath, 'codex');
377
+ const removableMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
378
+ .filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
379
+ if (removableMatches.length > 0) {
380
+ await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(deferredCleanup.detection, removableMatches.map((prompt) => prompt.path)));
381
+ }
382
+ const blockedMatches = getLegacyGlobalPromptMatches(deferredCleanup.detection)
383
+ .filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
384
+ if (blockedMatches.length > 0) {
385
+ console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
386
+ for (const prompt of blockedMatches) {
387
+ console.log(chalk.dim(` - ${prompt.toolId}: ${prompt.path}`));
388
+ }
389
+ console.log();
390
+ }
391
+ }
392
+ /**
393
+ * Reads the currently installed workflow IDs for a single tool from the
394
+ * generated skill layout on disk.
395
+ */
396
+ async getInstalledWorkflowsForTool(projectPath, toolId) {
397
+ const tool = AI_TOOLS.find((candidate) => candidate.value === toolId);
398
+ if (!tool) {
399
+ return new Set();
400
+ }
401
+ return new Set(scanInstalledWorkflowsShared(projectPath, [tool]));
186
402
  }
187
403
  async performLegacyCleanup(projectPath, detection) {
188
404
  const spinner = ora('Cleaning up legacy files...').start();
@@ -301,7 +517,9 @@ export class InitCommand {
301
517
  if (tokens.length === 0) {
302
518
  throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
303
519
  }
304
- const normalizedTokens = tokens.map((token) => token.toLowerCase());
520
+ // Retired ids resolve to their current tool, so a rebrand does not break
521
+ // an existing `--tools windsurf` in someone's setup script.
522
+ const normalizedTokens = tokens.map((token) => resolveToolIdAlias(token.toLowerCase()));
305
523
  if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
306
524
  throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
307
525
  }
@@ -318,23 +536,34 @@ export class InitCommand {
318
536
  }
319
537
  return deduped;
320
538
  }
321
- validateTools(toolIds, toolStates) {
539
+ validateTools(toolIds, toolStates, projectPath) {
322
540
  const validatedTools = [];
323
- for (const toolId of toolIds) {
541
+ const reconciledToolIds = toolIds.includes('codex') && toolIds.includes('agents')
542
+ ? toolIds.filter((toolId) => toolId !== 'agents')
543
+ : toolIds;
544
+ if (reconciledToolIds.length !== toolIds.length) {
545
+ console.log(chalk.dim('Codex and agents share .agents/skills; writing one tree with Codex and generic skill references.'));
546
+ }
547
+ for (const toolId of reconciledToolIds) {
324
548
  const tool = AI_TOOLS.find((t) => t.value === toolId);
325
549
  if (!tool) {
326
550
  const validToolIds = getToolsWithSkillsDir();
327
551
  throw new Error(`Unknown tool '${toolId}'. Valid tools:\n ${validToolIds.join('\n ')}`);
328
552
  }
329
- if (!tool.skillsDir) {
553
+ if (!toolSupportsSkills(tool)) {
330
554
  const validToolsWithSkills = getToolsWithSkillsDir();
331
555
  throw new Error(`Tool '${toolId}' does not support skill generation.\nTools with skill generation support:\n ${validToolsWithSkills.join('\n ')}`);
332
556
  }
333
557
  const preState = toolStates.get(tool.value);
558
+ const skillsPath = resolveToolSkillsDir(projectPath, tool);
559
+ const isGlobalSkillTarget = hasGlobalSkillTarget(tool);
334
560
  validatedTools.push({
335
561
  value: tool.value,
336
562
  name: tool.name,
337
563
  skillsDir: tool.skillsDir,
564
+ skillsPath,
565
+ skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
566
+ isGlobalSkillTarget,
338
567
  wasConfigured: preState?.configured ?? false,
339
568
  });
340
569
  }
@@ -353,6 +582,7 @@ export class InitCommand {
353
582
  path.join(openspecPath, 'changes', 'archive'),
354
583
  ];
355
584
  for (const dir of directories) {
585
+ FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
356
586
  await FileSystemUtils.createDirectory(dir);
357
587
  }
358
588
  return;
@@ -365,6 +595,7 @@ export class InitCommand {
365
595
  path.join(openspecPath, 'changes', 'archive'),
366
596
  ];
367
597
  for (const dir of directories) {
598
+ FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), dir);
368
599
  await FileSystemUtils.createDirectory(dir);
369
600
  }
370
601
  spinner.stopAndPersist({
@@ -375,11 +606,20 @@ export class InitCommand {
375
606
  // ═══════════════════════════════════════════════════════════
376
607
  // SKILL & COMMAND GENERATION
377
608
  // ═══════════════════════════════════════════════════════════
378
- async generateSkillsAndCommands(projectPath, tools) {
609
+ /**
610
+ * Generates skill files and slash commands for each selected tool,
611
+ * honoring the configured delivery mode (skills, commands, or both).
612
+ *
613
+ * @param projectPath - Absolute path to the project root
614
+ * @param tools - Selected tools with their skill directory metadata
615
+ * @returns Created, refreshed, and failed tools plus removed artifact counts
616
+ */
617
+ async generateSkillsAndCommands(projectPath, tools, writeCopilotCloud) {
379
618
  const createdTools = [];
380
619
  const refreshedTools = [];
381
620
  const failedTools = [];
382
621
  const commandsSkipped = [];
622
+ const skillsInvocableCommandSkips = [];
383
623
  let removedCommandCount = 0;
384
624
  let removedSkillCount = 0;
385
625
  // Read global config for profile and delivery settings (use --profile override if set)
@@ -388,33 +628,35 @@ export class InitCommand {
388
628
  const delivery = globalConfig.delivery ?? 'both';
389
629
  const workflows = getProfileWorkflows(profile, globalConfig.workflows);
390
630
  // Get skill and command templates filtered by profile workflows
391
- const shouldGenerateSkills = delivery !== 'commands';
392
- const shouldGenerateCommands = delivery !== 'skills';
393
- const skillTemplates = shouldGenerateSkills ? getSkillTemplates(workflows) : [];
394
- const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : [];
631
+ const deliveryIncludesCommands = delivery !== 'skills';
632
+ const skillTemplates = getSkillTemplates(workflows);
633
+ const commandContents = getCommandContents(workflows);
395
634
  // Process each tool
396
635
  for (const tool of tools) {
397
636
  const spinner = ora(`Setting up ${tool.name}...`).start();
398
637
  try {
399
- // Generate skill files if delivery includes skills
638
+ const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
639
+ const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
640
+ // Generate skill files if the selected delivery and tool capability allow skills
400
641
  if (shouldGenerateSkills) {
401
- // Use tool-specific skillsDir
402
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
403
642
  // Create skill directories and SKILL.md files
404
643
  for (const { template, dirName } of skillTemplates) {
405
- const skillDir = path.join(skillsDir, dirName);
644
+ const skillDir = path.join(tool.skillsPath, dirName);
406
645
  const skillFile = path.join(skillDir, 'SKILL.md');
407
646
  // Generate SKILL.md content with YAML frontmatter including generatedBy
408
- // Use hyphen-based command references for tools where filename = command name
409
- const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
647
+ const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
410
648
  const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
411
649
  // Write the skill file
650
+ FileSystemUtils.assertPathWithin(tool.skillsRoot, skillFile);
412
651
  await FileSystemUtils.writeFile(skillFile, skillContent);
413
652
  }
653
+ writeSharedSkillTarget(projectPath, tool.value);
414
654
  }
415
- if (!shouldGenerateSkills) {
416
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
417
- removedSkillCount += await this.removeSkillDirs(skillsDir);
655
+ if (shouldRemoveSkillsForTool(tool.value, delivery) && !tool.isGlobalSkillTarget) {
656
+ removedSkillCount += await this.removeSkillDirs(tool.skillsRoot, tool.skillsPath);
657
+ // Retain an explicit selection even when this delivery mode produces
658
+ // no skills, so a divergent legacy sibling cannot reclaim ownership.
659
+ writeSharedSkillTarget(projectPath, tool.value);
418
660
  }
419
661
  // Generate commands if delivery includes commands
420
662
  if (shouldGenerateCommands) {
@@ -422,17 +664,25 @@ export class InitCommand {
422
664
  if (adapter) {
423
665
  const generatedCommands = generateCommands(commandContents, adapter);
424
666
  for (const cmd of generatedCommands) {
425
- const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
667
+ const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
426
668
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
427
669
  }
428
670
  }
671
+ }
672
+ else if (deliveryIncludesCommands) {
673
+ if (resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
674
+ skillsInvocableCommandSkips.push(tool.value);
675
+ }
429
676
  else {
430
677
  commandsSkipped.push(tool.value);
431
678
  }
432
679
  }
433
- if (!shouldGenerateCommands) {
680
+ if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
434
681
  removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
435
682
  }
683
+ if (tool.value === 'github-copilot' && writeCopilotCloud) {
684
+ await writeCopilotCloudFiles(projectPath);
685
+ }
436
686
  spinner.succeed(`Setup complete for ${tool.name}`);
437
687
  if (tool.wasConfigured) {
438
688
  refreshedTools.push(tool);
@@ -446,11 +696,22 @@ export class InitCommand {
446
696
  failedTools.push({ name: tool.name, error: error });
447
697
  }
448
698
  }
699
+ for (const tool of [...createdTools, ...refreshedTools]) {
700
+ for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
701
+ if (hasMovableContent(migration)) {
702
+ console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
703
+ }
704
+ const kept = keptInPlaceNotice(migration);
705
+ if (kept)
706
+ console.log(chalk.dim(kept));
707
+ }
708
+ }
449
709
  return {
450
710
  createdTools,
451
711
  refreshedTools,
452
712
  failedTools,
453
713
  commandsSkipped,
714
+ skillsInvocableCommandSkips,
454
715
  removedCommandCount,
455
716
  removedSkillCount,
456
717
  };
@@ -468,6 +729,7 @@ export class InitCommand {
468
729
  }
469
730
  try {
470
731
  const yamlContent = serializeConfig({ schema: DEFAULT_SCHEMA });
732
+ FileSystemUtils.assertProjectArtifactPath(path.dirname(openspecPath), configPath);
471
733
  await FileSystemUtils.writeFile(configPath, yamlContent);
472
734
  return 'created';
473
735
  }
@@ -478,9 +740,9 @@ export class InitCommand {
478
740
  // ═══════════════════════════════════════════════════════════
479
741
  // UI & OUTPUT
480
742
  // ═══════════════════════════════════════════════════════════
481
- displaySuccessMessage(projectPath, tools, results, configStatus) {
743
+ displaySuccessMessage(projectPath, tools, results, configStatus, copilot) {
482
744
  console.log();
483
- console.log(chalk.bold('OpenSpec Setup Complete'));
745
+ console.log(chalk.bold(results.failedTools.length > 0 ? 'OpenSpec Setup Incomplete' : 'OpenSpec Setup Complete'));
484
746
  console.log();
485
747
  // Show created vs refreshed tools
486
748
  if (results.createdTools.length > 0) {
@@ -496,17 +758,56 @@ export class InitCommand {
496
758
  const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
497
759
  const delivery = globalConfig.delivery ?? 'both';
498
760
  const workflows = getProfileWorkflows(profile, globalConfig.workflows);
499
- const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', ');
500
- const skillCount = delivery !== 'commands' ? getSkillTemplates(workflows).length : 0;
501
- const commandCount = delivery !== 'skills' ? getCommandContents(workflows).length : 0;
502
- if (skillCount > 0 && commandCount > 0) {
503
- console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
504
- }
505
- else if (skillCount > 0) {
506
- console.log(`${skillCount} skills in ${toolDirs}/`);
761
+ const usesGlobalSkillTarget = successfulTools.some((tool) => tool.isGlobalSkillTarget);
762
+ if (!usesGlobalSkillTarget) {
763
+ const toolDirs = [
764
+ ...new Set(successfulTools
765
+ .map((tool) => tool.skillsDir)
766
+ .filter((skillsDir) => Boolean(skillsDir))),
767
+ ].join(', ');
768
+ const skillCount = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, delivery))
769
+ ? getSkillTemplates(workflows).length
770
+ : 0;
771
+ const commandCount = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, delivery))
772
+ ? getCommandContents(workflows).length
773
+ : 0;
774
+ if (skillCount > 0 && commandCount > 0) {
775
+ console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
776
+ }
777
+ else if (skillCount > 0) {
778
+ console.log(`${skillCount} skills in ${toolDirs}/`);
779
+ }
780
+ else if (commandCount > 0) {
781
+ console.log(`${commandCount} commands in ${toolDirs}/`);
782
+ }
507
783
  }
508
- else if (commandCount > 0) {
509
- console.log(`${commandCount} commands in ${toolDirs}/`);
784
+ else {
785
+ const skillTools = successfulTools.filter((tool) => shouldGenerateSkillsForTool(tool.value, delivery));
786
+ const skillCount = skillTools.length * getSkillTemplates(workflows).length;
787
+ if (skillCount > 0) {
788
+ const skillDirs = [...new Set(skillTools.map((tool) => tool.skillsPath))];
789
+ console.log(`${skillCount} skills in ${skillDirs.join(', ')}`);
790
+ }
791
+ const commandContents = getCommandContents(workflows);
792
+ const commandTools = successfulTools.filter((tool) => shouldGenerateCommandsForTool(tool.value, delivery));
793
+ const commandCount = commandTools.length * commandContents.length;
794
+ if (commandCount > 0) {
795
+ const commandDirs = [
796
+ ...new Set(commandTools.flatMap((tool) => {
797
+ const adapter = CommandAdapterRegistry.get(tool.value);
798
+ if (!adapter)
799
+ return [];
800
+ return commandContents.map((command) => {
801
+ const commandPath = adapter.getFilePath(command.id);
802
+ const absolutePath = path.isAbsolute(commandPath)
803
+ ? commandPath
804
+ : path.join(projectPath, commandPath);
805
+ return path.dirname(absolutePath);
806
+ });
807
+ })),
808
+ ];
809
+ console.log(`${commandCount} commands in ${commandDirs.join(', ')}`);
810
+ }
510
811
  }
511
812
  }
512
813
  // Show failures
@@ -517,12 +818,42 @@ export class InitCommand {
517
818
  if (results.commandsSkipped.length > 0) {
518
819
  console.log(chalk.dim(`Commands skipped for: ${results.commandsSkipped.join(', ')} (no adapter)`));
519
820
  }
821
+ if (results.skillsInvocableCommandSkips.length > 0) {
822
+ console.log(chalk.dim(`Commands skipped for: ${results.skillsInvocableCommandSkips.join(', ')} (uses skills)`));
823
+ }
520
824
  if (results.removedCommandCount > 0) {
521
825
  console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`));
522
826
  }
523
827
  if (results.removedSkillCount > 0) {
524
828
  console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
525
829
  }
830
+ // GitHub Copilot cloud files are opt-in — report what is actually on disk:
831
+ // list the managed files that now exist (never files we didn't write), flag
832
+ // any user-owned file we left untouched, note an opt-out cleanup, or (when
833
+ // skipped for want of a signal) say how to turn them on.
834
+ const copilotSucceeded = successfulTools.some((tool) => tool.value === 'github-copilot');
835
+ if (copilotSucceeded && copilot.write) {
836
+ if (copilot.present.length > 0) {
837
+ console.log(`GitHub Copilot cloud files: ${copilot.present.join(', ')}`);
838
+ }
839
+ if (copilot.collisions.length > 0) {
840
+ console.log(chalk.dim(`Left your existing ${copilot.collisions.join(' and ')} untouched — add the OpenSpec ` +
841
+ `install step by hand so the Copilot cloud agent can run openspec.`));
842
+ }
843
+ }
844
+ else if (copilotSucceeded && copilot.removed > 0) {
845
+ console.log(chalk.dim(`Removed: ${copilot.removed} Copilot cloud agent file(s) (opted out of cloud files)`));
846
+ }
847
+ else if (copilotSucceeded && copilot.skippedUndecided) {
848
+ console.log(chalk.dim("Skipped GitHub Copilot cloud files (opt-in). Enable with 'openspec init --copilot-cloud'."));
849
+ }
850
+ // Show manual setup notes for tools that need extra configuration
851
+ for (const tool of successfulTools) {
852
+ const setupNote = AI_TOOLS.find((t) => t.value === tool.value)?.setupNote;
853
+ if (setupNote) {
854
+ console.log(chalk.yellow(`Setup required for ${tool.name}: ${setupNote}`));
855
+ }
856
+ }
526
857
  // Config status
527
858
  if (configStatus === 'created') {
528
859
  console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
@@ -538,29 +869,98 @@ export class InitCommand {
538
869
  console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
539
870
  }
540
871
  // Getting started (task 7.6: show propose if in profile)
541
- const globalCfg = getGlobalConfig();
542
- const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
543
- const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
544
- console.log();
545
- if (activeWorkflows.includes('propose')) {
872
+ const activeWorkflows = this.getActiveWorkflows();
873
+ // When no tool got /opsx:* commands, point at the skill instead of a
874
+ // command that does not exist.
875
+ const activeDelivery = getGlobalConfig().delivery ?? 'both';
876
+ const commandsGenerated = successfulTools.some((tool) => shouldGenerateCommandsForTool(tool.value, activeDelivery));
877
+ const skillsGenerated = successfulTools.some((tool) => shouldGenerateSkillsForTool(tool.value, activeDelivery));
878
+ // Each hint line must be a usable instruction for the tool it serves.
879
+ // Tools that generated commands are told the command name their files
880
+ // answer to (/opsx:* when namespaced under opsx/, /opsx-* when the
881
+ // filename is the command); tools that only got skills are told their
882
+ // documented skill invocation (Kimi Code: /skill:openspec-*; Codex CLI:
883
+ // $openspec-*; others: /openspec-*). Tools that got no artifacts are
884
+ // covered by the configuration correction instead. When the selection
885
+ // disagrees, print one line per distinct instruction, labeled with the
886
+ // tools it applies to.
887
+ const startHintLines = (command) => {
888
+ const hintToTools = new Map();
889
+ for (const tool of successfulTools) {
890
+ let hint;
891
+ if (shouldGenerateCommandsForTool(tool.value, activeDelivery)) {
892
+ const transformer = getTransformerForTool(tool.value, activeDelivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
893
+ hint = `Start your first change: ${transformer ? transformer(command) : command} "your idea"`;
894
+ }
895
+ else if (shouldGenerateSkillsForTool(tool.value, activeDelivery)) {
896
+ const skillReference = getSkillReferenceTransformer(tool.value)(command);
897
+ // Tools with no slash surface (e.g. Rovo Dev) reference skills as
898
+ // prose ("the openspec-propose skill"); phrase the hint so it reads
899
+ // as an instruction rather than a dead command with an argument.
900
+ hint = usesNaturalLanguageSkillReferences(tool.value)
901
+ ? `Start your first change: ask ${tool.name} to use ${skillReference} with "your idea"`
902
+ : `Start your first change: ${skillReference} "your idea"`;
903
+ }
904
+ else {
905
+ continue;
906
+ }
907
+ hintToTools.set(hint, [...(hintToTools.get(hint) ?? []), tool.name]);
908
+ }
909
+ if (hintToTools.size === 0) {
910
+ // No successful tools: keep the generic command hint
911
+ return [`Start your first change: ${command} "your idea"`];
912
+ }
913
+ if (hintToTools.size === 1) {
914
+ return [[...hintToTools.keys()][0]];
915
+ }
916
+ return [...hintToTools.entries()].map(([hint, toolNames]) => `${hint} (${toolNames.join(', ')})`);
917
+ };
918
+ const printStartHints = (command) => {
546
919
  console.log(chalk.bold('Getting started:'));
547
- console.log(' Start your first change: /opsx:propose "your idea"');
920
+ for (const line of startHintLines(command)) {
921
+ console.log(` ${line}`);
922
+ }
923
+ };
924
+ console.log();
925
+ // delivery=commands with tools that only support skills: those tools get
926
+ // no artifacts at all, so print a per-tool configuration correction
927
+ // rather than leave them with a dead (or missing) instruction — even
928
+ // when other selected tools did get commands or skills.
929
+ const zeroArtifactTools = successfulTools.filter((tool) => !shouldGenerateSkillsForTool(tool.value, activeDelivery) &&
930
+ !shouldGenerateCommandsForTool(tool.value, activeDelivery));
931
+ if (zeroArtifactTools.length > 0) {
932
+ const names = zeroArtifactTools.map((tool) => tool.name).join(', ');
933
+ console.log(chalk.yellow(`No skills or commands were generated for ${names}: delivery is set to 'commands' but ` +
934
+ `${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
935
+ `Run 'openspec config set delivery both' to generate skills.`));
936
+ }
937
+ if (successfulTools.length > 0 && !commandsGenerated && !skillsGenerated) {
938
+ // Nothing was generated for any tool: the correction above is the
939
+ // whole story, so don't advertise an invocation that doesn't exist.
940
+ }
941
+ else if (activeWorkflows.includes('propose')) {
942
+ printStartHints('/opsx:propose');
548
943
  }
549
944
  else if (activeWorkflows.includes('new')) {
550
- console.log(chalk.bold('Getting started:'));
551
- console.log(' Start your first change: /opsx:new "your idea"');
945
+ printStartHints('/opsx:new');
552
946
  }
553
947
  else {
554
948
  console.log("Done. Run 'openspec config profile' to configure your workflows.");
555
949
  }
556
950
  // Links
557
951
  console.log();
558
- console.log(`Learn more: ${chalk.cyan('https://github.com/codewalla-engineering/OpenSpec')}`);
559
- console.log(`Feedback: ${chalk.cyan('https://github.com/codewalla-engineering/OpenSpec/issues')}`);
560
- // Restart instruction if any tools were configured
561
- if (results.createdTools.length > 0 || results.refreshedTools.length > 0) {
952
+ console.log(`Learn more: ${chalk.cyan('https://github.com/codewalla-engineering/openspec-upstream-sync')}`);
953
+ console.log(`Feedback: ${chalk.cyan('https://github.com/codewalla-engineering/openspec-upstream-sync/issues')}`);
954
+ // Restart instruction if any tools were configured and got a surface
955
+ // (when nothing was generated there is nothing a restart would pick up);
956
+ // only mention commands when commands were actually generated. Not "slash
957
+ // commands": Amazon Q's generated files are prompt-library entries invoked
958
+ // with @, so a restart line promising slash commands would be wrong for it.
959
+ if ((results.createdTools.length > 0 || results.refreshedTools.length > 0) && (commandsGenerated || skillsGenerated)) {
562
960
  console.log();
563
- console.log(chalk.white('Restart your IDE for slash commands to take effect.'));
961
+ console.log(chalk.white(commandsGenerated
962
+ ? 'Restart your IDE for the new commands to take effect.'
963
+ : 'Restart your IDE for the new skills to take effect.'));
564
964
  }
565
965
  console.log();
566
966
  }
@@ -572,18 +972,19 @@ export class InitCommand {
572
972
  spinner: PROGRESS_SPINNER,
573
973
  }).start();
574
974
  }
575
- async removeSkillDirs(skillsDir) {
975
+ async removeSkillDirs(skillsRoot, skillsDir) {
576
976
  let removed = 0;
577
977
  for (const workflow of ALL_WORKFLOWS) {
578
978
  const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
579
979
  if (!dirName)
580
980
  continue;
581
981
  const skillDir = path.join(skillsDir, dirName);
982
+ if (!fs.existsSync(skillDir))
983
+ continue;
984
+ FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
582
985
  try {
583
- if (fs.existsSync(skillDir)) {
584
- await fs.promises.rm(skillDir, { recursive: true, force: true });
585
- removed++;
586
- }
986
+ await fs.promises.rm(skillDir, { recursive: true, force: true });
987
+ removed++;
587
988
  }
588
989
  catch {
589
990
  // Ignore errors
@@ -598,7 +999,7 @@ export class InitCommand {
598
999
  return 0;
599
1000
  for (const workflow of ALL_WORKFLOWS) {
600
1001
  const cmdPath = adapter.getFilePath(workflow);
601
- const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
1002
+ const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
602
1003
  try {
603
1004
  if (fs.existsSync(fullPath)) {
604
1005
  await fs.promises.unlink(fullPath);