@codewalla_india/openspec 1.3.1 → 1.3.2

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 +272 -191
  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 +34 -0
  37. package/dist/comprehension-quiz/providers/anthropic.js +53 -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 +59 -0
  41. package/dist/comprehension-quiz/providers/interface.js +63 -0
  42. package/dist/comprehension-quiz/providers/local.d.ts +34 -0
  43. package/dist/comprehension-quiz/providers/local.js +53 -0
  44. package/dist/comprehension-quiz/providers/ollama.d.ts +34 -0
  45. package/dist/comprehension-quiz/providers/ollama.js +53 -0
  46. package/dist/comprehension-quiz/providers/openai.d.ts +34 -0
  47. package/dist/comprehension-quiz/providers/openai.js +53 -0
  48. package/dist/comprehension-quiz/question-generator.d.ts +69 -0
  49. package/dist/comprehension-quiz/question-generator.js +141 -0
  50. package/dist/comprehension-quiz/quiz-executor.d.ts +70 -0
  51. package/dist/comprehension-quiz/quiz-executor.js +165 -0
  52. package/dist/comprehension-quiz/types.d.ts +115 -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 +237 -115
  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
@@ -10,21 +10,24 @@ import ora from 'ora';
10
10
  import * as fs from 'fs';
11
11
  import { createRequire } from 'module';
12
12
  import { FileSystemUtils } from '../utils/file-system.js';
13
- import { transformToHyphenCommands } from '../utils/command-references.js';
13
+ import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillReferences } from '../utils/command-references.js';
14
14
  import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
15
15
  import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
16
- import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
17
- import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
16
+ import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.js';
17
+ import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
18
18
  import { isInteractive } from '../utils/interactive.js';
19
- import { setupTelemetryIdentity } from '../telemetry/index.js';
20
19
  import { getGlobalConfig } from './global-config.js';
21
- import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
20
+ import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
21
+ import { getOnboardingCommands } from './onboarding-commands.js';
22
22
  import { getAvailableTools } from './available-tools.js';
23
- import { WORKFLOW_TO_SKILL_DIR, getCommandConfiguredTools, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
24
- import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, } from './migration.js';
23
+ import { readIdentity, writeIdentity, promptForIdentity, validateIdentity, normalizeIdentity } from '../telemetry/identity.js';
24
+ import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
25
+ import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
26
+ import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
27
+ import { writeSharedSkillTarget } from './shared-skill-target.js';
28
+ import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
25
29
  const require = createRequire(import.meta.url);
26
30
  const { version: OPENSPEC_VERSION } = require('../../package.json');
27
- const OLD_CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'archive'];
28
31
  /**
29
32
  * Scans installed workflow artifacts (skills and managed commands) across all configured tools.
30
33
  * Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
@@ -42,6 +45,12 @@ export class UpdateCommand {
42
45
  constructor(options = {}) {
43
46
  this.force = options.force ?? false;
44
47
  }
48
+ /**
49
+ * Refreshes OpenSpec skills and commands for all configured tools,
50
+ * regenerating artifacts according to the effective profile and delivery mode.
51
+ *
52
+ * @param projectPath - Path to the project root containing the openspec directory
53
+ */
45
54
  async execute(projectPath) {
46
55
  const resolvedProjectPath = path.resolve(projectPath);
47
56
  const openspecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
@@ -49,8 +58,37 @@ export class UpdateCommand {
49
58
  if (!await FileSystemUtils.directoryExists(openspecPath)) {
50
59
  throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
51
60
  }
52
- await setupTelemetryIdentity({ interactive: isInteractive() });
53
- // 2. Perform one-time migration if needed before any legacy upgrade generation.
61
+ // 1.5. Prompt for identity if not already configured (task 8.1)
62
+ const existingIdentity = await readIdentity();
63
+ if (!existingIdentity && isInteractive()) {
64
+ const identity = await promptForIdentity();
65
+ if (identity) {
66
+ const normalized = normalizeIdentity(identity);
67
+ const validation = validateIdentity(normalized);
68
+ if (validation.valid) {
69
+ await writeIdentity(normalized);
70
+ }
71
+ else {
72
+ console.error(`Invalid identity: ${validation.errors.join(', ')}`);
73
+ process.exit(1);
74
+ }
75
+ }
76
+ else {
77
+ console.error('Identity is required for OpenSpec. Please provide an email or username.');
78
+ process.exit(1);
79
+ }
80
+ }
81
+ // 2. Migrate OpenSpec-managed skills left in renamed tool directories
82
+ // (e.g. .kimi -> .kimi-code) so they stay detected and get refreshed,
83
+ // then perform the one-time profile migration if needed before any
84
+ // legacy upgrade generation.
85
+ for (const migration of migrateLegacyToolDirs(resolvedProjectPath)) {
86
+ if (hasMovableContent(migration)) {
87
+ console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
88
+ }
89
+ this.reportKeptInPlace(migration);
90
+ }
91
+ const declinedMigrations = await this.offerConsentedLegacyMigrations(resolvedProjectPath);
54
92
  // Use detected tool directories to preserve existing opsx skills/commands.
55
93
  const detectedTools = getAvailableTools(resolvedProjectPath);
56
94
  migrateIfNeededShared(resolvedProjectPath, detectedTools);
@@ -60,31 +98,47 @@ export class UpdateCommand {
60
98
  const delivery = globalConfig.delivery ?? 'both';
61
99
  const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
62
100
  const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
63
- const shouldGenerateSkills = delivery !== 'commands';
64
- const shouldGenerateCommands = delivery !== 'skills';
65
101
  // 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config
66
- const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
102
+ const legacyUpgrade = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
103
+ const { newlyConfiguredTools, workflowOverrides: legacyWorkflowOverrides, deferredGlobalCleanup, } = legacyUpgrade;
67
104
  // 5. Find configured tools
68
105
  const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
106
+ const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
69
107
  if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
108
+ if (deferredGlobalCleanup) {
109
+ await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
110
+ }
111
+ if (declinedMigrations.length > 0) {
112
+ // Not an unconfigured project — a configured one the user chose to
113
+ // leave in its former directory. Saying "run init" would be wrong.
114
+ for (const migration of declinedMigrations) {
115
+ console.log(chalk.yellow(`Nothing to update: this project's OpenSpec files are still in ${migration.from}/, ` +
116
+ `which OpenSpec no longer writes.`));
117
+ console.log(chalk.dim(`Re-run "openspec update" and accept the move to ${migration.to}/ to resume updates.`));
118
+ }
119
+ return;
120
+ }
121
+ await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
70
122
  console.log(chalk.yellow('No configured tools found.'));
71
123
  console.log(chalk.dim('Run "openspec init" to set up tools.'));
72
124
  return;
73
125
  }
74
- // 6. Check version status for all configured tools
75
- const commandConfiguredTools = getCommandConfiguredTools(resolvedProjectPath);
76
- const commandConfiguredSet = new Set(commandConfiguredTools);
77
- const toolStatuses = configuredTools.map((toolId) => {
78
- const status = getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION);
79
- if (!status.configured && commandConfiguredSet.has(toolId)) {
80
- return { ...status, configured: true };
81
- }
82
- return status;
83
- });
126
+ // 6. Check version status for all configured tools, against the same workflow set
127
+ // the generation loop below writes — otherwise a legacy-upgraded tool would be
128
+ // fingerprinted against commands it was never given.
129
+ const toolStatuses = configuredTools.map((toolId) => getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION, {
130
+ workflows: legacyWorkflowOverrides[toolId] ?? desiredWorkflows,
131
+ }));
84
132
  const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
85
133
  // 7. Smart update detection
86
134
  const toolsNeedingVersionUpdate = toolStatuses
87
- .filter((s) => s.needsUpdate)
135
+ .filter((s) => {
136
+ if (!s.needsUpdate || delivery !== 'commands') {
137
+ return s.needsUpdate;
138
+ }
139
+ const tool = AI_TOOLS.find((candidate) => candidate.value === s.toolId);
140
+ return !tool || !hasGlobalSkillTarget(tool);
141
+ })
88
142
  .map((s) => s.toolId);
89
143
  const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
90
144
  const toolsToUpdateSet = new Set([
@@ -92,56 +146,82 @@ export class UpdateCommand {
92
146
  ...toolsNeedingConfigSync,
93
147
  ]);
94
148
  const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
95
- if (!this.force && toolsToUpdateSet.size === 0) {
149
+ if (!this.force && toolsToUpdateSet.size === 0 && newlyConfiguredTools.length === 0) {
150
+ if (deferredGlobalCleanup) {
151
+ await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
152
+ }
96
153
  // All tools are up to date
97
154
  this.displayUpToDateMessage(toolStatuses);
155
+ await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
98
156
  // Still check for new tool directories and extra workflows
99
157
  this.detectNewTools(resolvedProjectPath, configuredTools);
100
158
  this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
101
- this.displayOldCoreCustomProfileNote(profile, globalConfig.workflows);
159
+ this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);
160
+ this.displaySetupNotes(configuredTools);
102
161
  return;
103
162
  }
104
163
  // 8. Display update plan
105
164
  if (this.force) {
106
165
  console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
107
166
  }
167
+ else if (toolsToUpdateSet.size === 0) {
168
+ console.log('No additional refresh needed after legacy migration.');
169
+ }
108
170
  else {
109
171
  this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
110
172
  }
111
173
  console.log();
112
174
  // 9. Determine what to generate based on delivery
113
- const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
114
- const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
175
+ const deliveryIncludesCommands = delivery !== 'skills';
115
176
  // 10. Update tools (all if force, otherwise only those needing update)
116
177
  const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
117
178
  const updatedTools = [];
118
179
  const failedTools = [];
180
+ const skillsInvocableCommandSkips = [];
181
+ const zeroArtifactTools = [];
119
182
  let removedCommandCount = 0;
120
183
  let removedSkillCount = 0;
121
184
  let removedDeselectedCommandCount = 0;
122
185
  let removedDeselectedSkillCount = 0;
123
186
  for (const toolId of toolsToUpdate) {
124
187
  const tool = AI_TOOLS.find((t) => t.value === toolId);
125
- if (!tool?.skillsDir)
188
+ if (!tool || !toolSupportsSkills(tool))
126
189
  continue;
127
190
  const spinner = ora(`Updating ${tool.name}...`).start();
128
191
  try {
129
- const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
192
+ const skillsDir = resolveToolSkillsDir(resolvedProjectPath, tool);
193
+ const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
194
+ const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
195
+ const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
196
+ const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
197
+ const skillTemplates = getSkillTemplates(toolWorkflows);
198
+ const commandContents = getCommandContents(toolWorkflows);
130
199
  // Generate skill files if delivery includes skills
131
200
  if (shouldGenerateSkills) {
132
201
  for (const { template, dirName } of skillTemplates) {
133
202
  const skillDir = path.join(skillsDir, dirName);
134
203
  const skillFile = path.join(skillDir, 'SKILL.md');
135
- // Use hyphen-based command references for OpenCode
136
- const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
204
+ const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
137
205
  const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
206
+ FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
138
207
  await FileSystemUtils.writeFile(skillFile, skillContent);
139
208
  }
140
- removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, desiredWorkflows);
209
+ writeSharedSkillTarget(resolvedProjectPath, tool.value);
210
+ removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows);
141
211
  }
142
212
  // Delete skill directories if delivery is commands-only
143
- if (!shouldGenerateSkills) {
144
- removedSkillCount += await this.removeSkillDirs(skillsDir);
213
+ if (shouldRemoveSkillsForTool(tool.value, delivery) && !hasGlobalSkillTarget(tool)) {
214
+ removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir);
215
+ // Persist the selected owner even when commands-only delivery leaves
216
+ // this target with no generated skills.
217
+ writeSharedSkillTarget(resolvedProjectPath, tool.value);
218
+ // A tool with no command adapter now has zero OpenSpec artifacts;
219
+ // say so like init does, rather than deleting its skills silently
220
+ // and letting tool detection re-suggest an init that would also
221
+ // generate nothing under this delivery setting.
222
+ if (!shouldGenerateCommandsForTool(tool.value, delivery)) {
223
+ zeroArtifactTools.push(tool.name);
224
+ }
145
225
  }
146
226
  // Generate commands if delivery includes commands
147
227
  if (shouldGenerateCommands) {
@@ -149,18 +229,27 @@ export class UpdateCommand {
149
229
  if (adapter) {
150
230
  const generatedCommands = generateCommands(commandContents, adapter);
151
231
  for (const cmd of generatedCommands) {
152
- const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
232
+ const commandFile = FileSystemUtils.resolveProjectArtifactPath(resolvedProjectPath, cmd.path);
153
233
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
154
234
  }
155
- removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, desiredWorkflows);
235
+ removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
156
236
  }
157
237
  }
238
+ else if (deliveryIncludesCommands && resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
239
+ skillsInvocableCommandSkips.push(tool.value);
240
+ }
158
241
  // Delete command files if delivery is skills-only
159
- if (!shouldGenerateCommands) {
242
+ if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
160
243
  removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
161
244
  }
162
245
  spinner.succeed(`Updated ${tool.name}`);
163
246
  updatedTools.push(tool.name);
247
+ for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
248
+ if (hasMovableContent(migration)) {
249
+ console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
250
+ }
251
+ this.reportKeptInPlace(migration);
252
+ }
164
253
  }
165
254
  catch (error) {
166
255
  spinner.fail(`Failed to update ${tool.name}`);
@@ -170,6 +259,9 @@ export class UpdateCommand {
170
259
  });
171
260
  }
172
261
  }
262
+ if (deferredGlobalCleanup) {
263
+ await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
264
+ }
173
265
  // 11. Summary
174
266
  console.log();
175
267
  if (updatedTools.length > 0) {
@@ -178,34 +270,69 @@ export class UpdateCommand {
178
270
  if (failedTools.length > 0) {
179
271
  console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
180
272
  }
273
+ if (skillsInvocableCommandSkips.length > 0) {
274
+ console.log(chalk.dim(`Commands skipped for: ${skillsInvocableCommandSkips.join(', ')} (uses skills)`));
275
+ }
181
276
  if (removedCommandCount > 0) {
182
277
  console.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
183
278
  }
184
279
  if (removedSkillCount > 0) {
185
280
  console.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
186
281
  }
282
+ if (zeroArtifactTools.length > 0) {
283
+ const names = zeroArtifactTools.join(', ');
284
+ console.log(chalk.yellow(`No skills or commands remain for ${names}: delivery is set to 'commands' but ` +
285
+ `${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
286
+ `Run 'openspec config set delivery both' to generate skills.`));
287
+ }
187
288
  if (removedDeselectedCommandCount > 0) {
188
289
  console.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
189
290
  }
190
291
  if (removedDeselectedSkillCount > 0) {
191
292
  console.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
192
293
  }
193
- // 12. Show onboarding message for newly configured tools from legacy upgrade
294
+ // 12. Show onboarding message for newly configured tools from legacy upgrade.
295
+ // Command tools get the command name their files answer to, skill-only
296
+ // tools their documented skill invocation, and disagreements fall back to
297
+ // naming the skill.
194
298
  if (newlyConfiguredTools.length > 0) {
299
+ const referenceFor = (command) => {
300
+ const neutralForm = `the ${transformToSkillReferences(command).slice(1)} skill`;
301
+ const forms = new Set(newlyConfiguredTools.map((toolId) => {
302
+ if (shouldGenerateCommandsForTool(toolId, delivery)) {
303
+ // Name the command the tool's files actually answer to:
304
+ // /opsx-<id> where the filename is the command name.
305
+ const transformer = getTransformerForTool(toolId, delivery, resolveCommandSurfaceCapability(toolId), resolveCommandInvocation(toolId));
306
+ return transformer ? transformer(command) : command;
307
+ }
308
+ return getSkillReferenceTransformer(toolId)(command);
309
+ }));
310
+ return forms.size === 1 ? [...forms][0] : neutralForm;
311
+ };
312
+ // Only hint at workflows these tools actually received. A legacy upgrade
313
+ // can install a narrower set than the profile (inferred Codex prompts).
314
+ const installedWorkflows = [
315
+ ...new Set(newlyConfiguredTools.flatMap((toolId) => legacyWorkflowOverrides[toolId] ?? desiredWorkflows)),
316
+ ];
317
+ const entries = getOnboardingCommands(installedWorkflows).map(({ command, description }) => [referenceFor(command), description]);
195
318
  console.log();
196
- console.log(chalk.bold('Getting started:'));
197
- console.log(' /opsx:new Start a new change');
198
- console.log(' /opsx:continue Create the next artifact');
199
- console.log(' /opsx:apply Implement tasks');
200
- console.log();
201
- console.log(`Learn more: ${chalk.cyan('https://github.com/codewalla-engineering/OpenSpec')}`);
319
+ if (entries.length > 0) {
320
+ const width = Math.max(...entries.map(([reference]) => reference.length));
321
+ console.log(chalk.bold('Getting started:'));
322
+ for (const [reference, description] of entries) {
323
+ console.log(` ${reference.padEnd(width)} ${description}`);
324
+ }
325
+ console.log();
326
+ }
327
+ console.log(`Learn more: ${chalk.cyan('https://github.com/codewalla-engineering/openspec-upstream-sync')}`);
202
328
  }
203
- const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
329
+ await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
204
330
  // 13. Detect new tool directories not currently configured
205
331
  this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
206
332
  // 14. Display note about extra workflows not in profile
207
333
  this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
208
- this.displayOldCoreCustomProfileNote(profile, globalConfig.workflows);
334
+ this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);
335
+ this.displaySetupNotes(configuredAndNewTools);
209
336
  // 15. List affected tools
210
337
  if (updatedTools.length > 0) {
211
338
  const toolDisplayNames = updatedTools;
@@ -213,6 +340,51 @@ export class UpdateCommand {
213
340
  }
214
341
  console.log();
215
342
  console.log(chalk.dim('Restart your IDE for changes to take effect.'));
343
+ if (failedTools.length > 0) {
344
+ throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
345
+ }
346
+ }
347
+ async syncCopilotCloudFiles(projectPath, configuredTools) {
348
+ try {
349
+ if (includesGitHubCopilot(configuredTools)) {
350
+ // Cloud files are opt-in (see cloud-agent.ts). `update` never prompts,
351
+ // so it only refreshes files the user has already opted into (via
352
+ // `openspec init` or a `githubCopilot.cloudAgent: true` config), or that
353
+ // a pre-opt-in project already has. Opting in is a deliberate init/config
354
+ // step, never a silent side effect of running update.
355
+ if (await isCopilotCloudEnabled(projectPath)) {
356
+ await writeCopilotCloudFiles(projectPath);
357
+ const collisions = await findUnmanagedCloudFiles(projectPath);
358
+ if (collisions.length > 0) {
359
+ console.log(chalk.dim(`Left your existing ${collisions.join(' and ')} untouched — add the OpenSpec ` +
360
+ `install step by hand so the Copilot cloud agent can run openspec.`));
361
+ }
362
+ return;
363
+ }
364
+ // Explicit opt-out (githubCopilot.cloudAgent: false) means "not here":
365
+ // remove any managed files a prior opt-in left behind (customized files
366
+ // are preserved). If the user simply never decided, stay quiet unless
367
+ // we're at an interactive terminal, where a one-line hint aids discovery.
368
+ if (readCopilotCloudOptIn(projectPath) === false) {
369
+ const removed = await removeCopilotCloudFiles(projectPath);
370
+ if (removed > 0) {
371
+ console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (opted out of cloud files)`));
372
+ }
373
+ }
374
+ else if (isInteractive()) {
375
+ console.log(chalk.dim("GitHub Copilot cloud coding-agent files are available (opt-in). Enable with 'openspec init --copilot-cloud'."));
376
+ }
377
+ return;
378
+ }
379
+ const removed = await removeCopilotCloudFiles(projectPath);
380
+ if (removed > 0) {
381
+ console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (github-copilot not configured)`));
382
+ }
383
+ }
384
+ catch (error) {
385
+ const message = error instanceof Error ? error.message : String(error);
386
+ console.warn(`Warning: failed to sync Copilot cloud agent files: ${message}`);
387
+ }
216
388
  }
217
389
  /**
218
390
  * Display message when all tools are up to date.
@@ -242,6 +414,18 @@ export class UpdateCommand {
242
414
  console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
243
415
  }
244
416
  }
417
+ /**
418
+ * Shows manual setup notes for configured tools that need extra
419
+ * configuration before they pick up generated files.
420
+ */
421
+ displaySetupNotes(toolIds) {
422
+ for (const toolId of toolIds) {
423
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
424
+ if (tool?.setupNote) {
425
+ console.log(chalk.yellow(`Setup required for ${tool.name}: ${tool.setupNote}`));
426
+ }
427
+ }
428
+ }
245
429
  /**
246
430
  * Detects new tool directories that aren't currently configured and displays a hint.
247
431
  */
@@ -270,38 +454,41 @@ export class UpdateCommand {
270
454
  }
271
455
  }
272
456
  /**
273
- * Suggest opting back into core when a custom profile still matches the old
274
- * pre-sync core set. Keep custom profiles user-owned; do not mutate them.
457
+ * Point out core workflows a custom profile is missing, so releases that
458
+ * grow CORE_WORKFLOWS stay discoverable. Keep custom profiles user-owned;
459
+ * do not mutate them.
275
460
  */
276
- displayOldCoreCustomProfileNote(profile, workflows) {
461
+ displayMissingCoreWorkflowsNote(profile, workflows) {
277
462
  if (profile !== 'custom' || !workflows) {
278
463
  return;
279
464
  }
280
465
  const workflowSet = new Set(workflows);
281
- const matchesOldCore = workflowSet.size === OLD_CORE_WORKFLOWS.length &&
282
- OLD_CORE_WORKFLOWS.every((workflow) => workflowSet.has(workflow));
283
- if (!matchesOldCore) {
466
+ const missing = CORE_WORKFLOWS.filter((workflow) => !workflowSet.has(workflow));
467
+ if (missing.length === 0) {
284
468
  return;
285
469
  }
286
- console.log(chalk.dim('Note: The core profile now includes sync. Your custom profile is preserving the old core workflow set.'));
287
- console.log(chalk.dim('Run `openspec config profile core` and then `openspec update` to add sync.'));
470
+ const label = missing.length === 1 ? 'workflow' : 'workflows';
471
+ const pronoun = missing.length === 1 ? 'it' : 'them';
472
+ console.log(chalk.dim(`Note: Your custom profile is missing ${missing.length} core ${label}: ${missing.join(', ')}`));
473
+ console.log(chalk.dim(`Run \`openspec config profile\` to add ${pronoun}, or \`openspec config profile core\` to use the core set.`));
288
474
  }
289
475
  /**
290
476
  * Removes skill directories for workflows when delivery changed to commands-only.
291
477
  * Returns the number of directories removed.
292
478
  */
293
- async removeSkillDirs(skillsDir) {
479
+ async removeSkillDirs(skillsRoot, skillsDir) {
294
480
  let removed = 0;
295
481
  for (const workflow of ALL_WORKFLOWS) {
296
482
  const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
297
483
  if (!dirName)
298
484
  continue;
299
485
  const skillDir = path.join(skillsDir, dirName);
486
+ if (!fs.existsSync(skillDir))
487
+ continue;
488
+ FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
300
489
  try {
301
- if (fs.existsSync(skillDir)) {
302
- await fs.promises.rm(skillDir, { recursive: true, force: true });
303
- removed++;
304
- }
490
+ await fs.promises.rm(skillDir, { recursive: true, force: true });
491
+ removed++;
305
492
  }
306
493
  catch {
307
494
  // Ignore errors
@@ -313,7 +500,7 @@ export class UpdateCommand {
313
500
  * Removes skill directories for workflows that are no longer selected in the active profile.
314
501
  * Returns the number of directories removed.
315
502
  */
316
- async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) {
503
+ async removeUnselectedSkillDirs(skillsRoot, skillsDir, desiredWorkflows) {
317
504
  const desiredSet = new Set(desiredWorkflows);
318
505
  let removed = 0;
319
506
  for (const workflow of ALL_WORKFLOWS) {
@@ -323,11 +510,12 @@ export class UpdateCommand {
323
510
  if (!dirName)
324
511
  continue;
325
512
  const skillDir = path.join(skillsDir, dirName);
513
+ if (!fs.existsSync(skillDir))
514
+ continue;
515
+ FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
326
516
  try {
327
- if (fs.existsSync(skillDir)) {
328
- await fs.promises.rm(skillDir, { recursive: true, force: true });
329
- removed++;
330
- }
517
+ await fs.promises.rm(skillDir, { recursive: true, force: true });
518
+ removed++;
331
519
  }
332
520
  catch {
333
521
  // Ignore errors
@@ -346,7 +534,7 @@ export class UpdateCommand {
346
534
  return 0;
347
535
  for (const workflow of ALL_WORKFLOWS) {
348
536
  const cmdPath = adapter.getFilePath(workflow);
349
- const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
537
+ const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
350
538
  try {
351
539
  if (fs.existsSync(fullPath)) {
352
540
  await fs.promises.unlink(fullPath);
@@ -373,7 +561,7 @@ export class UpdateCommand {
373
561
  if (desiredSet.has(workflow))
374
562
  continue;
375
563
  const cmdPath = adapter.getFilePath(workflow);
376
- const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
564
+ const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
377
565
  try {
378
566
  if (fs.existsSync(fullPath)) {
379
567
  await fs.promises.unlink(fullPath);
@@ -386,6 +574,72 @@ export class UpdateCommand {
386
574
  }
387
575
  return removed;
388
576
  }
577
+ /**
578
+ * Offers to move OpenSpec content out of a renamed tool's former directory
579
+ * when the old location might still be the live one — today, Windsurf's
580
+ * `.windsurf/` after the Devin Desktop rebrand.
581
+ *
582
+ * Interactive runs are asked, because nothing on disk distinguishes a user
583
+ * who took the rebrand from one still on a pre-rebrand Windsurf build that
584
+ * reads only `.windsurf/`. `--force` and non-interactive runs migrate, which
585
+ * is what an unattended upgrade wants.
586
+ */
587
+ /** Surfaces files the move left behind rather than overwriting. */
588
+ reportKeptInPlace(migration) {
589
+ const notice = keptInPlaceNotice(migration);
590
+ if (notice)
591
+ console.log(chalk.dim(notice));
592
+ }
593
+ async offerConsentedLegacyMigrations(projectPath) {
594
+ const pending = findLegacyToolMigrations(projectPath).filter((m) => m.needsConsent);
595
+ const declined = [];
596
+ if (pending.length === 0)
597
+ return declined;
598
+ for (const migration of pending) {
599
+ // Nothing movable: every legacy file differs from its counterpart, so
600
+ // there is no move to offer. Still say so — silence would leave two
601
+ // divergent copies the user never hears about.
602
+ if (!hasMovableContent(migration)) {
603
+ this.reportKeptInPlace(migration);
604
+ console.log();
605
+ continue;
606
+ }
607
+ console.log(chalk.yellow(legacyMigrationNotice(migration)));
608
+ if (!this.force && isInteractive()) {
609
+ const { confirm } = await import('@inquirer/prompts');
610
+ let shouldMigrate;
611
+ try {
612
+ shouldMigrate = await confirm({
613
+ message: `Move ${describeLegacyMigration(migration)} from ${migration.from}/ to ${migration.to}/?`,
614
+ default: true,
615
+ });
616
+ }
617
+ catch {
618
+ // Closed stdin is not consent, and it must not abort the update.
619
+ shouldMigrate = false;
620
+ }
621
+ if (!shouldMigrate) {
622
+ // Say what declining costs. OpenSpec writes the current root now, so
623
+ // the files keep working where they are, but OpenSpec stops managing
624
+ // them — it no longer looks in the former directory.
625
+ console.log(chalk.dim(`Left in place. OpenSpec writes ${migration.to}/ now and will not manage ` +
626
+ `${migration.from}/, so those files stay as they are until you move them. ` +
627
+ `You will be asked again next run.`));
628
+ console.log();
629
+ declined.push(migration);
630
+ continue;
631
+ }
632
+ }
633
+ for (const applied of migrateLegacyToolDirs(projectPath, [migration.toolId])) {
634
+ if (hasMovableContent(applied)) {
635
+ console.log(chalk.dim(`Migrated ${describeLegacyMigration(applied)}: ${applied.from} → ${applied.to}`));
636
+ }
637
+ this.reportKeptInPlace(applied);
638
+ }
639
+ console.log();
640
+ }
641
+ return declined;
642
+ }
389
643
  /**
390
644
  * Detect and handle legacy OpenSpec artifacts.
391
645
  * Unlike init, update warns but continues if legacy files found in non-interactive mode.
@@ -395,25 +649,37 @@ export class UpdateCommand {
395
649
  // Detect legacy artifacts
396
650
  const detection = await detectLegacyArtifacts(projectPath);
397
651
  if (!detection.hasLegacyArtifacts) {
398
- return []; // No legacy artifacts found
652
+ return { newlyConfiguredTools: [], workflowOverrides: {} }; // No legacy artifacts found
399
653
  }
400
654
  // Show what was detected
401
- console.log();
402
- console.log(formatDetectionSummary(detection));
403
- console.log();
655
+ const immediateSummary = formatDetectionSummary(omitGlobalLegacyPromptFiles(detection));
656
+ const deferredSummary = formatDeferredGlobalPromptSummary(detection);
657
+ if (immediateSummary || deferredSummary) {
658
+ console.log();
659
+ if (immediateSummary) {
660
+ console.log(immediateSummary);
661
+ console.log();
662
+ }
663
+ if (deferredSummary) {
664
+ console.log(deferredSummary);
665
+ console.log();
666
+ }
667
+ }
404
668
  const canPrompt = isInteractive();
405
669
  if (this.force) {
406
- // --force flag: proceed with cleanup automatically
407
- await this.performLegacyCleanup(projectPath, detection);
408
- // Then upgrade legacy tools to new skills
409
- return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
670
+ const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
671
+ await this.performImmediateLegacyCleanup(projectPath, detection);
672
+ return {
673
+ ...legacyUpgrade,
674
+ deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
675
+ };
410
676
  }
411
677
  if (!canPrompt) {
412
678
  // Non-interactive mode without --force: warn and continue
413
679
  // (Unlike init, update doesn't abort - user may just want to update skills)
414
680
  console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
415
681
  console.log();
416
- return [];
682
+ return { newlyConfiguredTools: [], workflowOverrides: {} };
417
683
  }
418
684
  // Interactive mode: prompt for confirmation
419
685
  const { confirm } = await import('@inquirer/prompts');
@@ -422,14 +688,47 @@ export class UpdateCommand {
422
688
  default: true,
423
689
  });
424
690
  if (shouldCleanup) {
425
- await this.performLegacyCleanup(projectPath, detection);
426
- // Then upgrade legacy tools to new skills
427
- return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
691
+ const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
692
+ await this.performImmediateLegacyCleanup(projectPath, detection);
693
+ return {
694
+ ...legacyUpgrade,
695
+ deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
696
+ };
428
697
  }
429
698
  else {
430
699
  console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
431
700
  console.log();
432
- return [];
701
+ return { newlyConfiguredTools: [], workflowOverrides: {} };
702
+ }
703
+ }
704
+ /**
705
+ * Cleans approved repo-local legacy artifacts before configured tools refresh.
706
+ */
707
+ async performImmediateLegacyCleanup(projectPath, detection) {
708
+ const immediateDetection = omitGlobalLegacyPromptFiles(detection);
709
+ if (immediateDetection.hasLegacyArtifacts) {
710
+ await this.performLegacyCleanup(projectPath, immediateDetection);
711
+ }
712
+ }
713
+ /**
714
+ * Cleans approved global Codex prompts after configured tools refresh so newly
715
+ * installed replacement skills can retire their prompts in the same run.
716
+ */
717
+ async performDeferredGlobalPromptCleanup(projectPath, detection) {
718
+ const availableCodexWorkflows = new Set(scanInstalledWorkflows(projectPath, ['codex']));
719
+ const removableMatches = getLegacyGlobalPromptMatches(detection)
720
+ .filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
721
+ if (removableMatches.length > 0) {
722
+ await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(detection, removableMatches.map((prompt) => prompt.path)));
723
+ }
724
+ const blockedMatches = getLegacyGlobalPromptMatches(detection)
725
+ .filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
726
+ if (blockedMatches.length > 0) {
727
+ console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
728
+ for (const prompt of blockedMatches) {
729
+ console.log(chalk.dim(` - ${prompt.toolId}: ${prompt.path}`));
730
+ }
731
+ console.log();
433
732
  }
434
733
  }
435
734
  /**
@@ -447,14 +746,14 @@ export class UpdateCommand {
447
746
  console.log();
448
747
  }
449
748
  /**
450
- * Upgrade legacy tools to new skills system.
451
- * Returns array of tool IDs that were newly configured.
749
+ * Upgrades unconfigured legacy tools into the skills-based setup and carries
750
+ * workflow overrides for migrations that should mirror legacy Codex prompts.
452
751
  */
453
752
  async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) {
454
753
  // Get tools that had legacy artifacts
455
754
  const legacyTools = getToolsFromLegacyArtifacts(detection);
456
755
  if (legacyTools.length === 0) {
457
- return [];
756
+ return { newlyConfiguredTools: [], workflowOverrides: {} };
458
757
  }
459
758
  // Get currently configured tools
460
759
  const configuredTools = getConfiguredToolsForProfileSync(projectPath);
@@ -462,13 +761,13 @@ export class UpdateCommand {
462
761
  // Filter to tools that aren't already configured
463
762
  const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
464
763
  if (unconfiguredLegacyTools.length === 0) {
465
- return [];
764
+ return { newlyConfiguredTools: [], workflowOverrides: {} };
466
765
  }
467
766
  // Get valid tools (those with skillsDir)
468
767
  const validToolIds = new Set(getToolsWithSkillsDir());
469
768
  const validUnconfiguredTools = unconfiguredLegacyTools.filter((t) => validToolIds.has(t));
470
769
  if (validUnconfiguredTools.length === 0) {
471
- return [];
770
+ return { newlyConfiguredTools: [], workflowOverrides: {} };
472
771
  }
473
772
  // Show what tools were detected from legacy artifacts
474
773
  console.log(chalk.bold('Tools detected from legacy artifacts:'));
@@ -504,32 +803,42 @@ export class UpdateCommand {
504
803
  if (selectedTools.length === 0) {
505
804
  console.log(chalk.dim('Skipping tool setup.'));
506
805
  console.log();
507
- return [];
806
+ return { newlyConfiguredTools: [], workflowOverrides: {} };
508
807
  }
509
808
  }
809
+ const inferredCodexWorkflows = getLegacyWorkflowIdsForTool(detection, 'codex');
510
810
  // Create skills/commands for selected tools using effective profile+delivery.
511
811
  const newlyConfigured = [];
512
- const shouldGenerateSkills = delivery !== 'commands';
513
- const shouldGenerateCommands = delivery !== 'skills';
514
- const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
515
- const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
812
+ const workflowOverrides = {};
516
813
  for (const toolId of selectedTools) {
517
814
  const tool = AI_TOOLS.find((t) => t.value === toolId);
518
- if (!tool?.skillsDir)
815
+ if (!tool || !toolSupportsSkills(tool))
519
816
  continue;
520
817
  const spinner = ora(`Setting up ${tool.name}...`).start();
521
818
  try {
522
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
819
+ const skillsDir = resolveToolSkillsDir(projectPath, tool);
820
+ const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : projectPath;
821
+ const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
822
+ const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
823
+ const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
824
+ ? inferredCodexWorkflows
825
+ : desiredWorkflows);
826
+ if (tool.value === 'codex' && inferredCodexWorkflows.length > 0) {
827
+ workflowOverrides[tool.value] = inferredCodexWorkflows;
828
+ }
829
+ const skillTemplates = getSkillTemplates(toolWorkflows);
830
+ const commandContents = getCommandContents(toolWorkflows);
523
831
  // Create skill files when delivery includes skills
524
832
  if (shouldGenerateSkills) {
525
833
  for (const { template, dirName } of skillTemplates) {
526
834
  const skillDir = path.join(skillsDir, dirName);
527
835
  const skillFile = path.join(skillDir, 'SKILL.md');
528
- // Use hyphen-based command references for OpenCode
529
- const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
836
+ const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
530
837
  const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
838
+ FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
531
839
  await FileSystemUtils.writeFile(skillFile, skillContent);
532
840
  }
841
+ writeSharedSkillTarget(projectPath, tool.value);
533
842
  }
534
843
  // Create commands when delivery includes commands
535
844
  if (shouldGenerateCommands) {
@@ -537,13 +846,19 @@ export class UpdateCommand {
537
846
  if (adapter) {
538
847
  const generatedCommands = generateCommands(commandContents, adapter);
539
848
  for (const cmd of generatedCommands) {
540
- const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
849
+ const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
541
850
  await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
542
851
  }
543
852
  }
544
853
  }
545
854
  spinner.succeed(`Setup complete for ${tool.name}`);
546
855
  newlyConfigured.push(toolId);
856
+ for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
857
+ if (hasMovableContent(migration)) {
858
+ console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
859
+ }
860
+ this.reportKeptInPlace(migration);
861
+ }
547
862
  }
548
863
  catch (error) {
549
864
  spinner.fail(`Failed to set up ${tool.name}`);
@@ -553,7 +868,7 @@ export class UpdateCommand {
553
868
  if (newlyConfigured.length > 0) {
554
869
  console.log();
555
870
  }
556
- return newlyConfigured;
871
+ return { newlyConfiguredTools: newlyConfigured, workflowOverrides };
557
872
  }
558
873
  }
559
874
  //# sourceMappingURL=update.js.map