@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
@@ -7,8 +7,11 @@
7
7
  * - `--store <id>` selects a registered store's root.
8
8
  * - Without `--store`, the nearest ancestor containing `openspec/` wins.
9
9
  * Leftover workspace view state is never considered a root here.
10
- * - With no nearest root, registered stores produce a selection hint error;
11
- * otherwise commands may treat the current directory as an implicit root.
10
+ * - With no nearest root, a global `defaultStore` (if set) is the last
11
+ * machine-level fallback before the selection hint error.
12
+ * - With no nearest root and no default, registered stores produce a
13
+ * selection hint error; otherwise commands may treat the current
14
+ * directory as an implicit root.
12
15
  *
13
16
  * Diagnostic codes reuse the store taxonomy where an error passes
14
17
  * through unchanged (`invalid_store_id`, metadata parse failures);
@@ -26,6 +29,7 @@ import { getStoreRootForBackend } from './store/registry.js';
26
29
  import { inspectOpenSpecRoot } from './openspec-root.js';
27
30
  import { findRepoPlanningRootSync } from './planning-home.js';
28
31
  import { classifyOpenSpecDir, storePointerProblem } from './project-config.js';
32
+ import { getGlobalConfig } from './global-config.js';
29
33
  import { FileSystemUtils } from '../utils/file-system.js';
30
34
  export class RootSelectionError extends Error {
31
35
  diagnostic;
@@ -221,6 +225,32 @@ async function resolveNearestOrDeclaredRoot(nearestRoot, globalDataDir) {
221
225
  throw error;
222
226
  }
223
227
  }
228
+ /**
229
+ * The machine-level fallback: the global `defaultStore` resolved as a root,
230
+ * with its own provenance (`global_default`) so JSON surfaces can tell a
231
+ * machine-wide default from a repo's `store:` pointer. Mirrors the
232
+ * declared-pointer catch — a stale or unregistered id degrades to the
233
+ * underlying error, reshaped to point at clearing the global default
234
+ * rather than passing --store.
235
+ */
236
+ async function resolveDefaultStoreRoot(id, globalDataDir) {
237
+ try {
238
+ return await resolveStoreRoot(id, globalDataDir, 'global_default');
239
+ }
240
+ catch (error) {
241
+ if (error instanceof RootSelectionError) {
242
+ const staleFix = error.diagnostic.code === 'unknown_store' ||
243
+ error.diagnostic.code === 'no_registered_stores'
244
+ ? `Register the store (openspec store register <path> --id ${id}) or clear the stale global default (openspec config unset defaultStore).`
245
+ : error.diagnostic.fix;
246
+ throw new RootSelectionError(`Global defaultStore '${id}': ${error.message}`, error.diagnostic.code, {
247
+ ...(error.diagnostic.target ? { target: error.diagnostic.target } : {}),
248
+ ...(staleFix ? { fix: staleFix } : {}),
249
+ });
250
+ }
251
+ throw error;
252
+ }
253
+ }
224
254
  export async function resolveOpenSpecRoot(options = {}) {
225
255
  if (options.storePath !== undefined) {
226
256
  throw new RootSelectionError('--store-path is not supported. Register the path with openspec store register <path>, then select it with --store <id>.', 'store_path_not_supported', {
@@ -236,6 +266,13 @@ export async function resolveOpenSpecRoot(options = {}) {
236
266
  if (nearestRoot) {
237
267
  return resolveNearestOrDeclaredRoot(nearestRoot, options.globalDataDir);
238
268
  }
269
+ // Machine-level fallback: a global defaultStore is consulted only after
270
+ // --store, the nearest local root, and project-level pointers have all
271
+ // failed to resolve — it changes the failure path, never the precedence.
272
+ const defaultStore = getGlobalConfig().defaultStore;
273
+ if (defaultStore) {
274
+ return resolveDefaultStoreRoot(defaultStore, options.globalDataDir);
275
+ }
239
276
  let registry;
240
277
  try {
241
278
  registry = await readStoreRegistryState(options.globalDataDir ? { globalDataDir: options.globalDataDir } : {});
@@ -265,9 +302,9 @@ export function toRootOutput(root) {
265
302
  };
266
303
  }
267
304
  /**
268
- * A store-selected root — explicit `--store` or the declared fallback.
269
- * Cross-root behavior (absolute paths, --store hints, suppressed
270
- * noun-form suggestions) keys on this, never on `source` directly.
305
+ * A store-selected root — explicit `--store`, a declared pointer, or the
306
+ * global default. Cross-root behavior (absolute paths, --store hints,
307
+ * suppressed noun-form suggestions) keys on this, never on `source` directly.
271
308
  */
272
309
  export function isStoreSelectedRoot(root) {
273
310
  return root.storeId !== undefined;
@@ -4,9 +4,14 @@ export const ScenarioSchema = z.object({
4
4
  rawText: z.string().min(1, VALIDATION_MESSAGES.SCENARIO_EMPTY),
5
5
  });
6
6
  export const RequirementSchema = z.object({
7
+ // SHALL/MUST body-keyword enforcement lives in the imperative validator
8
+ // (Validator.applySpecRules), not here: the parser collapses the requirement
9
+ // header into `text`, so a Zod refine on `text` cannot tell "keyword in header
10
+ // only" from "keyword in body" and emits a misleading generic error. The
11
+ // validator recovers the header and emits the targeted hint for both the
12
+ // main-spec and change-delta paths (#1156).
7
13
  text: z.string()
8
- .min(1, VALIDATION_MESSAGES.REQUIREMENT_EMPTY)
9
- .refine((text) => text.includes('SHALL') || text.includes('MUST'), VALIDATION_MESSAGES.REQUIREMENT_NO_SHALL),
14
+ .min(1, VALIDATION_MESSAGES.REQUIREMENT_EMPTY),
10
15
  scenarios: z.array(ScenarioSchema)
11
16
  .min(1, VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS),
12
17
  });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pre-approved tools for generated skills and slash commands, emitted as the
3
+ * `allowed-tools` frontmatter field (Agent Skills standard for SKILL.md;
4
+ * same field for Claude Code slash commands). Scoped to the OpenSpec CLI so
5
+ * agents that honor it stop prompting on each `openspec` call; the field
6
+ * only pre-approves — it does not restrict — so any other tool a skill or
7
+ * command needs (Read, Write, arbitrary Bash for builds/tests) stays
8
+ * available under the user's normal permission settings. Tools that don't
9
+ * recognize the field ignore it.
10
+ */
11
+ export declare const OPENSPEC_CLI_ALLOWED_TOOLS = "Bash(openspec:*)";
12
+ //# sourceMappingURL=allowed-tools.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pre-approved tools for generated skills and slash commands, emitted as the
3
+ * `allowed-tools` frontmatter field (Agent Skills standard for SKILL.md;
4
+ * same field for Claude Code slash commands). Scoped to the OpenSpec CLI so
5
+ * agents that honor it stop prompting on each `openspec` call; the field
6
+ * only pre-approves — it does not restrict — so any other tool a skill or
7
+ * command needs (Read, Write, arbitrary Bash for builds/tests) stays
8
+ * available under the user's normal permission settings. Tools that don't
9
+ * recognize the field ignore it.
10
+ */
11
+ export const OPENSPEC_CLI_ALLOWED_TOOLS = 'Bash(openspec:*)';
12
+ //# sourceMappingURL=allowed-tools.js.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export { SKILL_NAMES, type SkillName, COMMAND_IDS, type CommandId, type ToolSkillStatus, type ToolVersionStatus, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
7
7
  export { type SkillTemplateEntry, type CommandTemplateEntry, getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
8
+ export { type SkillCapableTool, toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export { SKILL_NAMES, COMMAND_IDS, getToolsWithSkillsDir, getToolSkillStatus, getToolStates, extractGeneratedByVersion, getToolVersionStatus, getConfiguredTools, getAllToolVersionStatus, } from './tool-detection.js';
7
7
  export { getSkillTemplates, getCommandTemplates, getCommandContents, generateSkillContent, } from './skill-generation.js';
8
+ export { toolSupportsSkills, getSkillCapableTools, hasGlobalSkillTarget, resolveToolSkillsDir, } from './skill-paths.js';
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns whether a legacy Codex skill differs from the current canonical
3
+ * replacement only by generated version, checkout line endings/BOM, or the
4
+ * known Codex/generic dual-reference expansion.
5
+ */
6
+ export declare function isLegacyCodexSkillEquivalentToCurrent(legacyContent: string, currentContent: string): boolean;
7
+ //# sourceMappingURL=skill-content-equivalence.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { OPENSPEC_SKILL_NAMES } from '../config.js';
2
+ const GENERATED_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3
+ const OPENSPEC_SKILL_NAME_SET = new Set(OPENSPEC_SKILL_NAMES);
4
+ /**
5
+ * Normalizes checkout line endings and a valid generated version inside the
6
+ * YAML frontmatter. Free-form `generatedBy` text in the instructions remains
7
+ * material.
8
+ */
9
+ function normalizeGeneratedSkill(content) {
10
+ const normalized = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
11
+ const frontmatter = normalized.match(/^---\n[\s\S]*?\n---(?:\n|$)/)?.[0];
12
+ if (!frontmatter)
13
+ return normalized;
14
+ const versionLine = /^(\s*generatedBy:\s*)(?:"([^"\n]+)"|'([^'\n]+)'|([^\s"'#]+))\s*$/m;
15
+ const normalizedFrontmatter = frontmatter.replace(versionLine, (line, prefix, doubleQuoted, singleQuoted, bare) => {
16
+ const version = doubleQuoted ?? singleQuoted ?? bare;
17
+ return version && GENERATED_VERSION.test(version)
18
+ ? `${prefix}"<generated-version>"`
19
+ : line;
20
+ });
21
+ return normalizedFrontmatter + normalized.slice(frontmatter.length);
22
+ }
23
+ /**
24
+ * Converts only known generated dual references in current Codex content back
25
+ * to the direct syntax used by legacy `.codex` output.
26
+ */
27
+ function toLegacyCodexReferences(content) {
28
+ return content.replace(/\$(openspec-[a-z0-9-]+) \(Codex\) or \/\1 \(other agents\)/g, (match, skillName) => OPENSPEC_SKILL_NAME_SET.has(skillName) ? `$${skillName}` : match);
29
+ }
30
+ /**
31
+ * Returns whether a legacy Codex skill differs from the current canonical
32
+ * replacement only by generated version, checkout line endings/BOM, or the
33
+ * known Codex/generic dual-reference expansion.
34
+ */
35
+ export function isLegacyCodexSkillEquivalentToCurrent(legacyContent, currentContent) {
36
+ const normalizedLegacy = normalizeGeneratedSkill(legacyContent);
37
+ const normalizedCurrent = normalizeGeneratedSkill(currentContent);
38
+ return (normalizedLegacy === normalizedCurrent ||
39
+ normalizedLegacy === toLegacyCodexReferences(normalizedCurrent));
40
+ }
41
+ //# sourceMappingURL=skill-content-equivalence.js.map
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Shared utilities for generating skill and command files.
5
5
  */
6
- import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxProposeSkillTemplate, getModifyChangeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, getOpsxOnboardCommandTemplate, getOpsxProposeCommandTemplate, getOpsxModifyCommandTemplate, } from '../templates/skill-templates.js';
6
+ import { getExploreSkillTemplate, getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getUpdateChangeSkillTemplate, getModifyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getBulkArchiveChangeSkillTemplate, getVerifyChangeSkillTemplate, getOnboardSkillTemplate, getOpsxProposeSkillTemplate, getOpsxExploreCommandTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxUpdateCommandTemplate, getOpsxModifyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxBulkArchiveCommandTemplate, getOpsxVerifyCommandTemplate, getOpsxOnboardCommandTemplate, getOpsxProposeCommandTemplate, } from '../templates/skill-templates.js';
7
+ import { OPENSPEC_CLI_ALLOWED_TOOLS } from './allowed-tools.js';
7
8
  /**
8
9
  * Gets skill templates with their directory names, optionally filtered by workflow IDs.
9
10
  *
@@ -15,6 +16,7 @@ export function getSkillTemplates(workflowFilter) {
15
16
  { template: getNewChangeSkillTemplate(), dirName: 'openspec-new-change', workflowId: 'new' },
16
17
  { template: getContinueChangeSkillTemplate(), dirName: 'openspec-continue-change', workflowId: 'continue' },
17
18
  { template: getApplyChangeSkillTemplate(), dirName: 'openspec-apply-change', workflowId: 'apply' },
19
+ { template: getUpdateChangeSkillTemplate(), dirName: 'openspec-update-change', workflowId: 'update' },
18
20
  { template: getModifyChangeSkillTemplate(), dirName: 'openspec-modify-change', workflowId: 'modify' },
19
21
  { template: getFfChangeSkillTemplate(), dirName: 'openspec-ff-change', workflowId: 'ff' },
20
22
  { template: getSyncSpecsSkillTemplate(), dirName: 'openspec-sync-specs', workflowId: 'sync' },
@@ -40,6 +42,7 @@ export function getCommandTemplates(workflowFilter) {
40
42
  { template: getOpsxNewCommandTemplate(), id: 'new' },
41
43
  { template: getOpsxContinueCommandTemplate(), id: 'continue' },
42
44
  { template: getOpsxApplyCommandTemplate(), id: 'apply' },
45
+ { template: getOpsxUpdateCommandTemplate(), id: 'update' },
43
46
  { template: getOpsxModifyCommandTemplate(), id: 'modify' },
44
47
  { template: getOpsxFfCommandTemplate(), id: 'ff' },
45
48
  { template: getOpsxSyncCommandTemplate(), id: 'sync' },
@@ -81,18 +84,19 @@ export function generateSkillContent(template, generatedByVersion, transformInst
81
84
  const instructions = transformInstructions
82
85
  ? transformInstructions(template.instructions)
83
86
  : template.instructions;
84
- return `---
85
- name: ${template.name}
86
- description: ${template.description}
87
- license: ${template.license || 'MIT'}
88
- compatibility: ${template.compatibility || 'Requires openspec CLI.'}
89
- metadata:
90
- author: ${template.metadata?.author || 'openspec'}
91
- version: "${template.metadata?.version || '1.0'}"
92
- generatedBy: "${generatedByVersion}"
93
- ---
94
-
95
- ${instructions}
87
+ return `---
88
+ name: ${template.name}
89
+ description: ${template.description}
90
+ allowed-tools: ${OPENSPEC_CLI_ALLOWED_TOOLS}
91
+ license: ${template.license || 'MIT'}
92
+ compatibility: ${template.compatibility || 'Requires openspec CLI.'}
93
+ metadata:
94
+ author: ${template.metadata?.author || 'openspec'}
95
+ version: "${template.metadata?.version || '1.0'}"
96
+ generatedBy: "${generatedByVersion}"
97
+ ---
98
+
99
+ ${instructions}
96
100
  `;
97
101
  }
98
102
  //# sourceMappingURL=skill-generation.js.map
@@ -0,0 +1,13 @@
1
+ import { type AIToolOption } from '../config.js';
2
+ export type SkillCapableTool = AIToolOption & ({
3
+ skillsDir: string;
4
+ } | {
5
+ globalSkillsDir: string;
6
+ });
7
+ export declare function toolSupportsSkills(tool: AIToolOption): tool is SkillCapableTool;
8
+ export declare function getSkillCapableTools(): SkillCapableTool[];
9
+ export declare function hasGlobalSkillTarget(tool: AIToolOption): boolean;
10
+ export declare function resolveToolSkillsDir(projectRoot: string, tool: SkillCapableTool, options?: {
11
+ homeDir?: string;
12
+ }): string;
13
+ //# sourceMappingURL=skill-paths.d.ts.map
@@ -0,0 +1,23 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { AI_TOOLS } from '../config.js';
4
+ export function toolSupportsSkills(tool) {
5
+ return Boolean(tool.skillsDir || tool.globalSkillsDir);
6
+ }
7
+ export function getSkillCapableTools() {
8
+ return AI_TOOLS.filter(toolSupportsSkills);
9
+ }
10
+ export function hasGlobalSkillTarget(tool) {
11
+ return Boolean(tool.globalSkillsDir);
12
+ }
13
+ export function resolveToolSkillsDir(projectRoot, tool, options = {}) {
14
+ if (tool.globalSkillsDir) {
15
+ const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? os.homedir();
16
+ return path.join(homeDir, tool.globalSkillsDir, 'skills');
17
+ }
18
+ if (tool.skillsDir) {
19
+ return path.join(projectRoot, tool.skillsDir, 'skills');
20
+ }
21
+ throw new Error(`Tool '${tool.value}' does not support skill generation.`);
22
+ }
23
+ //# sourceMappingURL=skill-paths.js.map
@@ -6,12 +6,12 @@
6
6
  /**
7
7
  * Names of skill directories created by openspec init.
8
8
  */
9
- export declare const SKILL_NAMES: readonly ["openspec-explore", "openspec-new-change", "openspec-continue-change", "openspec-modify-change", "openspec-apply-change", "openspec-ff-change", "openspec-sync-specs", "openspec-archive-change", "openspec-bulk-archive-change", "openspec-verify-change", "openspec-onboard", "openspec-propose"];
9
+ export declare const SKILL_NAMES: readonly ["openspec-explore", "openspec-new-change", "openspec-continue-change", "openspec-apply-change", "openspec-update-change", "openspec-ff-change", "openspec-sync-specs", "openspec-archive-change", "openspec-bulk-archive-change", "openspec-verify-change", "openspec-onboard", "openspec-propose"];
10
10
  export type SkillName = (typeof SKILL_NAMES)[number];
11
11
  /**
12
12
  * IDs of command templates created by openspec init.
13
13
  */
14
- export declare const COMMAND_IDS: readonly ["explore", "new", "continue", "modify", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard", "propose"];
14
+ export declare const COMMAND_IDS: readonly ["explore", "new", "continue", "apply", "update", "ff", "sync", "archive", "bulk-archive", "verify", "onboard", "propose"];
15
15
  export type CommandId = (typeof COMMAND_IDS)[number];
16
16
  /**
17
17
  * Status of skill configuration for a tool.
@@ -32,9 +32,13 @@ export interface ToolVersionStatus {
32
32
  toolId: string;
33
33
  /** The tool's display name */
34
34
  toolName: string;
35
- /** Whether the tool has any skills configured */
35
+ /** Whether the tool has any skills or commands configured */
36
36
  configured: boolean;
37
- /** The generatedBy version found in the skill files, or null if not found */
37
+ /**
38
+ * The generatedBy version recorded in the tool's skill files. For a tool that
39
+ * has commands but no skills, the current version when the command files match
40
+ * what would be generated now. Null when neither says the files are current.
41
+ */
38
42
  generatedByVersion: string | null;
39
43
  /** Whether the tool needs updating (version mismatch or missing) */
40
44
  needsUpdate: boolean;
@@ -47,6 +51,19 @@ export declare function getToolsWithSkillsDir(): string[];
47
51
  * Checks which skill files exist for a tool.
48
52
  */
49
53
  export declare function getToolSkillStatus(projectRoot: string, toolId: string): ToolSkillStatus;
54
+ /**
55
+ * Checks whether a tool has at least one generated OpenSpec command file.
56
+ */
57
+ export declare function toolHasAnyConfiguredCommand(projectPath: string, toolId: string): boolean;
58
+ /**
59
+ * Checks whether command files for a tool on disk match current generated command contents.
60
+ *
61
+ * Command files carry no version stamp, so content equality is the only available
62
+ * "is this current?" signal for a commands-only install.
63
+ */
64
+ export declare function areCommandFilesUpToDate(projectRoot: string, toolId: string, options?: {
65
+ workflows?: readonly string[];
66
+ }): boolean;
50
67
  /**
51
68
  * Gets the skill status for all tools with skillsDir configured.
52
69
  */
@@ -57,11 +74,14 @@ export declare function getToolStates(projectRoot: string): Map<string, ToolSkil
57
74
  */
58
75
  export declare function extractGeneratedByVersion(skillFilePath: string): string | null;
59
76
  /**
60
- * Gets version status for a tool by reading the first available skill file.
77
+ * Gets version status for a tool by reading its skill files, falling back to a
78
+ * command-content fingerprint for installs that have commands but no skills.
61
79
  */
62
- export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string): ToolVersionStatus;
80
+ export declare function getToolVersionStatus(projectRoot: string, toolId: string, currentVersion: string, options?: {
81
+ workflows?: readonly string[];
82
+ }): ToolVersionStatus;
63
83
  /**
64
- * Gets all configured tools in the project.
84
+ * Gets all configured tools in the project (configured via skills or commands).
65
85
  */
66
86
  export declare function getConfiguredTools(projectRoot: string): string[];
67
87
  /**
@@ -5,24 +5,18 @@
5
5
  */
6
6
  import path from 'path';
7
7
  import * as fs from 'fs';
8
- import { AI_TOOLS } from '../config.js';
8
+ import { AI_TOOLS, OPENSPEC_SKILL_NAMES } from '../config.js';
9
+ import { CommandAdapterRegistry, generateCommands } from '../command-generation/index.js';
10
+ import { getCommandContents } from './skill-generation.js';
11
+ import { getGlobalConfig } from '../global-config.js';
12
+ import { getProfileWorkflows, ALL_WORKFLOWS } from '../profiles.js';
13
+ import { isSharedSkillTargetActive, readSharedSkillTarget, reconcileSharedSkillTargets, } from '../shared-skill-target.js';
14
+ import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, } from '../command-surface.js';
15
+ import { getSkillCapableTools, resolveToolSkillsDir, toolSupportsSkills, } from './skill-paths.js';
9
16
  /**
10
17
  * Names of skill directories created by openspec init.
11
18
  */
12
- export const SKILL_NAMES = [
13
- 'openspec-explore',
14
- 'openspec-new-change',
15
- 'openspec-continue-change',
16
- 'openspec-modify-change',
17
- 'openspec-apply-change',
18
- 'openspec-ff-change',
19
- 'openspec-sync-specs',
20
- 'openspec-archive-change',
21
- 'openspec-bulk-archive-change',
22
- 'openspec-verify-change',
23
- 'openspec-onboard',
24
- 'openspec-propose',
25
- ];
19
+ export const SKILL_NAMES = OPENSPEC_SKILL_NAMES;
26
20
  /**
27
21
  * IDs of command templates created by openspec init.
28
22
  */
@@ -30,8 +24,8 @@ export const COMMAND_IDS = [
30
24
  'explore',
31
25
  'new',
32
26
  'continue',
33
- 'modify',
34
27
  'apply',
28
+ 'update',
35
29
  'ff',
36
30
  'sync',
37
31
  'archive',
@@ -44,21 +38,26 @@ export const COMMAND_IDS = [
44
38
  * Gets the list of tools with skillsDir configured.
45
39
  */
46
40
  export function getToolsWithSkillsDir() {
47
- return AI_TOOLS.filter((t) => t.skillsDir).map((t) => t.value);
41
+ return getSkillCapableTools().map((tool) => tool.value);
48
42
  }
49
43
  /**
50
44
  * Checks which skill files exist for a tool.
51
45
  */
52
46
  export function getToolSkillStatus(projectRoot, toolId) {
53
47
  const tool = AI_TOOLS.find((t) => t.value === toolId);
54
- if (!tool?.skillsDir) {
48
+ if (!tool || !toolSupportsSkills(tool)) {
49
+ return { configured: false, fullyConfigured: false, skillCount: 0 };
50
+ }
51
+ if (tool.skillsDir && !isSharedSkillTargetActive(projectRoot, toolId)) {
55
52
  return { configured: false, fullyConfigured: false, skillCount: 0 };
56
53
  }
57
- const skillsDir = path.join(projectRoot, tool.skillsDir, 'skills');
54
+ const skillsDirs = [
55
+ resolveToolSkillsDir(projectRoot, tool),
56
+ ...(tool.legacySkillsDirs ?? []).map((root) => path.join(projectRoot, root, 'skills')),
57
+ ];
58
58
  let skillCount = 0;
59
59
  for (const skillName of SKILL_NAMES) {
60
- const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
61
- if (fs.existsSync(skillFile)) {
60
+ if (skillsDirs.some((skillsDir) => fs.existsSync(path.join(skillsDir, skillName, 'SKILL.md')))) {
62
61
  skillCount++;
63
62
  }
64
63
  }
@@ -68,14 +67,108 @@ export function getToolSkillStatus(projectRoot, toolId) {
68
67
  skillCount,
69
68
  };
70
69
  }
70
+ /**
71
+ * Checks whether a tool has at least one generated OpenSpec command file.
72
+ */
73
+ export function toolHasAnyConfiguredCommand(projectPath, toolId) {
74
+ const adapter = CommandAdapterRegistry.get(toolId);
75
+ if (!adapter)
76
+ return false;
77
+ for (const commandId of COMMAND_IDS) {
78
+ const cmdPath = adapter.getFilePath(commandId);
79
+ const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
80
+ if (fs.existsSync(fullPath)) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ }
86
+ /**
87
+ * Normalizes checkout artifacts that are not real content drift: a UTF-8 BOM and
88
+ * CRLF line endings, which a Windows clone with `core.autocrlf` reintroduces on
89
+ * every checkout of committed command files.
90
+ */
91
+ function normalizeCommandContent(content) {
92
+ return content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
93
+ }
94
+ /**
95
+ * Checks whether command files for a tool on disk match current generated command contents.
96
+ *
97
+ * Command files carry no version stamp, so content equality is the only available
98
+ * "is this current?" signal for a commands-only install.
99
+ */
100
+ export function areCommandFilesUpToDate(projectRoot, toolId, options) {
101
+ const adapter = CommandAdapterRegistry.get(toolId);
102
+ if (!adapter)
103
+ return false;
104
+ let workflows;
105
+ if (options?.workflows) {
106
+ workflows = options.workflows;
107
+ }
108
+ else {
109
+ try {
110
+ const globalCfg = getGlobalConfig();
111
+ const profile = globalCfg.profile ?? 'core';
112
+ workflows = getProfileWorkflows(profile, globalCfg.workflows);
113
+ }
114
+ catch {
115
+ workflows = ALL_WORKFLOWS;
116
+ }
117
+ }
118
+ const knownWorkflows = workflows.filter((w) => ALL_WORKFLOWS.includes(w));
119
+ const commandContents = getCommandContents(knownWorkflows);
120
+ const generatedCommands = generateCommands(commandContents, adapter);
121
+ if (generatedCommands.length === 0) {
122
+ return false;
123
+ }
124
+ for (const cmd of generatedCommands) {
125
+ const cmdPath = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectRoot, cmd.path);
126
+ if (!fs.existsSync(cmdPath)) {
127
+ return false;
128
+ }
129
+ try {
130
+ const existingContent = fs.readFileSync(cmdPath, 'utf-8');
131
+ if (normalizeCommandContent(existingContent) !== normalizeCommandContent(cmd.fileContent)) {
132
+ return false;
133
+ }
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ // Also check no extra command files exist for deselected workflows
140
+ const desiredWorkflowSet = new Set(knownWorkflows);
141
+ for (const workflow of ALL_WORKFLOWS) {
142
+ if (desiredWorkflowSet.has(workflow))
143
+ continue;
144
+ const cmdPath = adapter.getFilePath(workflow);
145
+ const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectRoot, cmdPath);
146
+ if (fs.existsSync(fullPath)) {
147
+ return false;
148
+ }
149
+ }
150
+ return true;
151
+ }
71
152
  /**
72
153
  * Gets the skill status for all tools with skillsDir configured.
73
154
  */
74
155
  export function getToolStates(projectRoot) {
75
156
  const states = new Map();
76
- const toolIds = AI_TOOLS.filter((t) => t.skillsDir).map((t) => t.value);
77
- for (const toolId of toolIds) {
78
- states.set(toolId, getToolSkillStatus(projectRoot, toolId));
157
+ const tools = getSkillCapableTools();
158
+ for (const tool of tools) {
159
+ const skillStatus = getToolSkillStatus(projectRoot, tool.value);
160
+ const markerConfigured = Boolean(tool.skillsDir) &&
161
+ readSharedSkillTarget(projectRoot, tool.skillsDir) === tool.value;
162
+ states.set(tool.value, markerConfigured
163
+ ? { ...skillStatus, configured: true }
164
+ : skillStatus);
165
+ }
166
+ const configuredTools = tools.filter((tool) => tool.skillsDir && states.get(tool.value)?.configured);
167
+ const activeSharedTargets = new Set(reconcileSharedSkillTargets(projectRoot, configuredTools).map((tool) => tool.value));
168
+ for (const tool of configuredTools) {
169
+ if (!activeSharedTargets.has(tool.value)) {
170
+ states.set(tool.value, { configured: false, fullyConfigured: false, skillCount: 0 });
171
+ }
79
172
  }
80
173
  return states;
81
174
  }
@@ -109,11 +202,12 @@ export function extractGeneratedByVersion(skillFilePath) {
109
202
  }
110
203
  }
111
204
  /**
112
- * Gets version status for a tool by reading the first available skill file.
205
+ * Gets version status for a tool by reading its skill files, falling back to a
206
+ * command-content fingerprint for installs that have commands but no skills.
113
207
  */
114
- export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
208
+ export function getToolVersionStatus(projectRoot, toolId, currentVersion, options) {
115
209
  const tool = AI_TOOLS.find((t) => t.value === toolId);
116
- if (!tool?.skillsDir) {
210
+ if (!tool || !toolSupportsSkills(tool)) {
117
211
  return {
118
212
  toolId,
119
213
  toolName: toolId,
@@ -122,17 +216,43 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
122
216
  needsUpdate: false,
123
217
  };
124
218
  }
125
- const skillsDir = path.join(projectRoot, tool.skillsDir, 'skills');
219
+ const skillsDirs = [
220
+ resolveToolSkillsDir(projectRoot, tool),
221
+ ...(tool.legacySkillsDirs ?? []).map((root) => path.join(projectRoot, root, 'skills')),
222
+ ];
126
223
  let generatedByVersion = null;
127
- // Find the first skill file that exists and read its version
224
+ let foundSkill = false;
225
+ // 1. Find the first skill file that exists and read its version
128
226
  for (const skillName of SKILL_NAMES) {
129
- const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
130
- if (fs.existsSync(skillFile)) {
131
- generatedByVersion = extractGeneratedByVersion(skillFile);
227
+ for (const skillsDir of skillsDirs) {
228
+ const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
229
+ if (fs.existsSync(skillFile)) {
230
+ generatedByVersion = extractGeneratedByVersion(skillFile);
231
+ foundSkill = true;
232
+ break;
233
+ }
234
+ }
235
+ if (foundSkill)
132
236
  break;
237
+ }
238
+ const skillConfigured = getToolSkillStatus(projectRoot, toolId).configured;
239
+ const commandConfigured = toolHasAnyConfiguredCommand(projectRoot, toolId);
240
+ const markerConfigured = Boolean(tool.skillsDir) &&
241
+ readSharedSkillTarget(projectRoot, tool.skillsDir) === toolId;
242
+ const configured = skillConfigured || commandConfigured || markerConfigured;
243
+ // 2. Commands-only installs have no skill file to read a version from, so fall
244
+ // back to comparing the generated command content. Deliberately skipped when
245
+ // skill files exist: an unreadable version there must still force a rewrite.
246
+ if (!skillConfigured && commandConfigured && areCommandFilesUpToDate(projectRoot, toolId, options)) {
247
+ generatedByVersion = currentVersion;
248
+ }
249
+ if (!skillConfigured && !commandConfigured && markerConfigured) {
250
+ const delivery = getGlobalConfig().delivery ?? 'both';
251
+ if (!shouldGenerateSkillsForTool(toolId, delivery) &&
252
+ !shouldGenerateCommandsForTool(toolId, delivery)) {
253
+ generatedByVersion = currentVersion;
133
254
  }
134
255
  }
135
- const configured = getToolSkillStatus(projectRoot, toolId).configured;
136
256
  const needsUpdate = configured && (generatedByVersion === null || generatedByVersion !== currentVersion);
137
257
  return {
138
258
  toolId,
@@ -143,12 +263,22 @@ export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
143
263
  };
144
264
  }
145
265
  /**
146
- * Gets all configured tools in the project.
266
+ * Gets all configured tools in the project (configured via skills or commands).
147
267
  */
148
268
  export function getConfiguredTools(projectRoot) {
149
- return AI_TOOLS
150
- .filter((t) => t.skillsDir && getToolSkillStatus(projectRoot, t.value).configured)
151
- .map((t) => t.value);
269
+ const configured = AI_TOOLS
270
+ .filter((t) => {
271
+ if (!toolSupportsSkills(t))
272
+ return false;
273
+ return (getToolSkillStatus(projectRoot, t.value).configured ||
274
+ toolHasAnyConfiguredCommand(projectRoot, t.value) ||
275
+ (Boolean(t.skillsDir) &&
276
+ readSharedSkillTarget(projectRoot, t.skillsDir) === t.value));
277
+ });
278
+ const activeProjectTools = new Set(reconcileSharedSkillTargets(projectRoot, configured.filter((tool) => tool.skillsDir)).map((tool) => tool.value));
279
+ return configured
280
+ .filter((tool) => tool.globalSkillsDir || activeProjectTools.has(tool.value))
281
+ .map((tool) => tool.value);
152
282
  }
153
283
  /**
154
284
  * Gets version status for all configured tools.
@@ -0,0 +1,16 @@
1
+ import { type AIToolOption } from './config.js';
2
+ /** Reads a valid-looking marker value without letting linked roots escape. */
3
+ export declare function readSharedSkillTarget(projectPath: string, skillsDir: string): string | undefined;
4
+ /**
5
+ * A shared skill root can only hold one rendered variant of each skill.
6
+ * Keep the writer recorded so later updates do not infer every tool that
7
+ * happens to use the same directory.
8
+ */
9
+ export declare function reconcileSharedSkillTargets(projectPath: string, tools: AIToolOption[]): AIToolOption[];
10
+ /**
11
+ * Returns whether a tool is the active writer for its physical skills root.
12
+ * Non-shared roots are always active.
13
+ */
14
+ export declare function isSharedSkillTargetActive(projectPath: string, toolId: string): boolean;
15
+ export declare function writeSharedSkillTarget(projectPath: string, toolId: string): void;
16
+ //# sourceMappingURL=shared-skill-target.d.ts.map