@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
package/dist/cli/index.js CHANGED
@@ -3,10 +3,11 @@ import { Command, Option } from 'commander';
3
3
  import { createRequire } from 'module';
4
4
  import ora from 'ora';
5
5
  import path from 'path';
6
- import { fileURLToPath } from 'url';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import { promises as fs } from 'fs';
8
- import { AI_TOOLS } from '../core/config.js';
8
+ import { AI_TOOLS, TOOL_ID_ALIASES } from '../core/config.js';
9
9
  import { UpdateCommand } from '../core/update.js';
10
+ import { getAvailableCliUpdate, displayCliUpdateNote, shouldOfferUpgrade, getInstallDir, offerCliUpgrade, rerunUpdateWithUpgradedCli, displayUpgradeCommand, isSourceCheckout, } from '../core/version-check.js';
10
11
  import { ListCommand } from '../core/list.js';
11
12
  import { ArchiveCommand } from '../core/archive.js';
12
13
  import { ViewCommand } from '../core/view.js';
@@ -23,10 +24,12 @@ import { registerStoreCommand } from '../commands/store.js';
23
24
  import { registerDoctorCommand } from '../commands/doctor.js';
24
25
  import { registerContextCommand } from '../commands/context.js';
25
26
  import { registerWorksetCommand } from '../commands/workset.js';
26
- import { statusCommand, instructionsCommand, applyInstructionsCommand, modifyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
27
- import { requireTelemetryIdentity, TelemetryIdentityRequiredError, trackCommand, shutdown } from '../telemetry/index.js';
28
- import { buildCommandTelemetryContext, resolveTelemetryCommandPath, } from '../telemetry/command-context.js';
27
+ import { createModifyCommand } from '../commands/modify.js';
28
+ import { statusCommand, instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
29
+ import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
30
+ import { readIdentity } from '../telemetry/identity.js';
29
31
  import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
32
+ import { isInteractive } from '../utils/interactive.js';
30
33
  const STORE_OPTION_DESCRIPTION = COMMON_FLAGS.store.description;
31
34
  // Deliberate rejection path: --store-path stays registered (hidden) so the
32
35
  // resolver can explain that registering the path is the supported route,
@@ -71,33 +74,6 @@ export function getCommandPath(command) {
71
74
  }
72
75
  return names.join(':') || 'openspec';
73
76
  }
74
- function commandUsesJson(command) {
75
- let current = command;
76
- while (current) {
77
- if (current.opts()?.json === true) {
78
- return true;
79
- }
80
- current = current.parent;
81
- }
82
- return false;
83
- }
84
- function exitTelemetryIdentityRequired(error, json) {
85
- if (json) {
86
- console.log(JSON.stringify({
87
- status: [
88
- {
89
- severity: 'error',
90
- code: error.code,
91
- message: error.message,
92
- },
93
- ],
94
- }, null, 2));
95
- }
96
- else {
97
- console.error(`Error: ${error.message}`);
98
- }
99
- process.exit(1);
100
- }
101
77
  program
102
78
  .name('openspec')
103
79
  .description('AI-native system for spec-driven development')
@@ -113,33 +89,43 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
113
89
  if (opts.color === false) {
114
90
  process.env.NO_COLOR = '1';
115
91
  }
116
- const commandPath = resolveTelemetryCommandPath(getCommandPath(actionCommand), actionCommand);
117
- const isBootstrap = commandPath === 'init' || commandPath === 'update';
118
- if (!isBootstrap) {
119
- try {
120
- await requireTelemetryIdentity();
121
- }
122
- catch (error) {
123
- if (error instanceof TelemetryIdentityRequiredError) {
124
- exitTelemetryIdentityRequired(error, commandUsesJson(actionCommand));
125
- }
126
- throw error;
92
+ // Identity gating: check for identity before executing command
93
+ // Exempt commands: init and update
94
+ const commandPath = getCommandPath(actionCommand);
95
+ const isExemptCommand = commandPath === 'init' || commandPath === 'update';
96
+ if (!isExemptCommand) {
97
+ const identity = await readIdentity();
98
+ if (!identity) {
99
+ console.error('Error: Identity required. Configure with: openspec init');
100
+ process.exitCode = 1;
101
+ process.exit(1);
127
102
  }
128
103
  }
129
- await trackCommand(commandPath, version, buildCommandTelemetryContext(actionCommand));
104
+ // Show first-run telemetry notice (if not seen)
105
+ await maybeShowTelemetryNotice();
106
+ // Track command execution (use actionCommand to get the actual subcommand)
107
+ await trackCommand(commandPath, version);
130
108
  });
131
109
  // Shutdown telemetry after command completes
132
110
  program.hook('postAction', async () => {
133
111
  await shutdown();
134
112
  });
135
- const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value);
136
- const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
113
+ const availableToolIds = AI_TOOLS
114
+ .filter((tool) => tool.skillsDir || tool.globalSkillsDir)
115
+ .map((tool) => tool.value);
116
+ const toolAliasNote = Object.entries(TOOL_ID_ALIASES)
117
+ .map(([retired, current]) => `${retired} (now ${current})`)
118
+ .join(', ');
119
+ const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}. Also accepted: ${toolAliasNote}`;
137
120
  program
138
121
  .command('init [path]')
139
122
  .description('Initialize OpenSpec in your project')
140
123
  .option('--tools <tools>', toolsOptionDescription)
141
124
  .option('--force', 'Auto-cleanup legacy files without prompting')
142
125
  .option('--profile <profile>', 'Override global config profile (core or custom)')
126
+ .option('--no-animation', 'Show a static welcome screen instead of the animated one')
127
+ .option('--copilot-cloud', 'Set up GitHub Copilot cloud coding-agent files without prompting')
128
+ .option('--no-copilot-cloud', 'Skip GitHub Copilot cloud coding-agent files without prompting')
143
129
  .action(async (targetPath = '.', options) => {
144
130
  try {
145
131
  // Validate that the path is a valid directory
@@ -167,6 +153,8 @@ program
167
153
  tools: options?.tools,
168
154
  force: options?.force,
169
155
  profile: options?.profile,
156
+ animation: options?.animation,
157
+ copilotCloud: options?.copilotCloud,
170
158
  });
171
159
  await initCommand.execute(targetPath);
172
160
  }
@@ -202,8 +190,55 @@ program
202
190
  .option('--force', 'Force update even when tools are up to date')
203
191
  .action(async (targetPath = '.', options) => {
204
192
  try {
193
+ const installDir = getInstallDir();
194
+ // Running from a clone: the version is whatever the branch says, so any
195
+ // upgrade advice would be noise. Decided before the request, so a
196
+ // contributor never waits on an answer that gets thrown away.
197
+ const latestVersion = isSourceCheckout(installDir) ? null : await getAvailableCliUpdate();
198
+ const announce = latestVersion !== null;
199
+ // Offer to upgrade first: this process generates files from its own
200
+ // templates, so upgrading afterwards would leave the old ones on disk.
201
+ // Both streams must be a terminal — with stdout redirected the question
202
+ // lands in the file and the user waits at a blank screen forever.
203
+ const canOffer = announce &&
204
+ shouldOfferUpgrade({
205
+ installDir,
206
+ projectPath: targetPath,
207
+ interactive: isInteractive(),
208
+ stdoutIsTty: Boolean(process.stdout.isTTY),
209
+ });
210
+ let declined = false;
211
+ if (latestVersion && canOffer) {
212
+ displayCliUpdateNote(latestVersion, targetPath, { withCommand: false });
213
+ const outcome = await offerCliUpgrade(latestVersion);
214
+ // Set the code and return rather than process.exit: exiting here would
215
+ // skip commander's postAction hook, killing the telemetry flush
216
+ // mid-request.
217
+ if (outcome === 'cancelled') {
218
+ // Ctrl-C means stop the command, not fall through to more prompts.
219
+ process.exitCode = 130;
220
+ return;
221
+ }
222
+ if (outcome === 'upgraded') {
223
+ process.exitCode = await rerunUpdateWithUpgradedCli(targetPath, {
224
+ force: options?.force,
225
+ });
226
+ return;
227
+ }
228
+ // Declined, failed, or upgraded-but-unreachable: fall through to the
229
+ // update, then leave the command on screen underneath it.
230
+ declined = true;
231
+ }
205
232
  const updateCommand = new UpdateCommand({ force: options?.force });
206
233
  await updateCommand.execute(targetPath);
234
+ if (declined) {
235
+ // The headline was printed before the prompt; only the manual route is
236
+ // still owed, and it belongs where the user is looking now.
237
+ displayUpgradeCommand(targetPath);
238
+ }
239
+ else if (latestVersion) {
240
+ displayCliUpdateNote(latestVersion, targetPath);
241
+ }
207
242
  }
208
243
  catch (error) {
209
244
  failWithError(error);
@@ -249,10 +284,19 @@ program
249
284
  program
250
285
  .command('view')
251
286
  .description('Display an interactive dashboard of specs and changes')
252
- .action(async () => {
287
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
288
+ .addOption(hiddenStorePathOption())
289
+ .action(async (options) => {
253
290
  try {
291
+ // Implicit cwd fallback stays enabled so `view` keeps accepting the same
292
+ // directories as `list`/`status` — notably pre-config.yaml `openspec/`
293
+ // dirs. ViewCommand still reports a missing openspec/ directory itself.
294
+ const root = await resolveRootForCommand(options ?? {});
295
+ if (!root) {
296
+ return;
297
+ }
254
298
  const viewCommand = new ViewCommand();
255
- await viewCommand.execute('.');
299
+ await viewCommand.execute(root.path);
256
300
  }
257
301
  catch (error) {
258
302
  failWithError(error);
@@ -497,27 +541,24 @@ program
497
541
  // Instructions command
498
542
  program
499
543
  .command('instructions [artifact]')
500
- .description('Output enriched instructions for creating an artifact or applying tasks')
544
+ .description('Output enriched instructions for artifacts, apply, archive, or modify')
501
545
  .option('--change <id>', 'Change name')
502
546
  .option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
503
547
  .option('--json', 'Output as JSON')
504
- .option('--record-comprehension-pass', 'Record a successful comprehension quiz pass (use with instructions apply)')
505
- .option('--score <percent>', 'Quiz score 0-100 (required with --record-comprehension-pass)', parseInt)
506
- .option('--attempt <n>', 'Quiz attempt number', parseInt)
507
- .option('--question-count <n>', 'Number of quiz questions taken', parseInt)
508
- .option('--artifact <id>', 'Source artifact to modify (use with: instructions modify)')
509
- .option('--workflow-input <text>', 'User modify request for telemetry (use with: instructions modify)')
510
- .option('--workflow-input-file <path>', 'Read modify request from a file (use with: instructions modify)')
511
- .option('--editor <tool>', 'AI editor used (cursor, windsurf, claude)')
512
548
  .option('--store <id>', STORE_OPTION_DESCRIPTION)
513
549
  .addOption(hiddenStorePathOption())
514
550
  .action(async (artifactId, options) => {
515
551
  try {
552
+ // Workflow instruction surfaces are reserved command branches, not artifacts.
516
553
  if (artifactId === 'apply') {
517
554
  await applyInstructionsCommand(options);
518
555
  }
556
+ else if (artifactId === 'archive') {
557
+ await archiveInstructionsCommand(options);
558
+ }
519
559
  else if (artifactId === 'modify') {
520
- await modifyInstructionsCommand(options);
560
+ const modifyCommand = createModifyCommand();
561
+ modifyCommand.parse(process.argv.slice(2));
521
562
  }
522
563
  else {
523
564
  await instructionsCommand(artifactId, options);
@@ -565,10 +606,6 @@ newCmd
565
606
  .option('--description <text>', 'Description to add to README.md')
566
607
  .option('--goal <text>', 'Optional goal metadata to store with the change')
567
608
  .option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`)
568
- .option('--entry-point <point>', 'Workflow entry point (propose, new, ff, manual)', 'manual')
569
- .option('--workflow-input <text>', 'User workflow intent for telemetry (verbatim chat/slash input)')
570
- .option('--workflow-input-file <path>', 'Read workflow intent from a file for telemetry')
571
- .option('--editor <tool>', 'AI editor used (cursor, windsurf, claude)')
572
609
  .option('--json', 'Output as JSON')
573
610
  .option('--store <id>', STORE_OPTION_DESCRIPTION)
574
611
  .addOption(hiddenStorePathOption())
@@ -31,9 +31,7 @@ export declare class ChangeCommand {
31
31
  json?: boolean;
32
32
  noInteractive?: boolean;
33
33
  }): Promise<void>;
34
- private getActiveChanges;
35
34
  private extractTitle;
36
- private countTasks;
37
35
  private printNextSteps;
38
36
  }
39
37
  //# sourceMappingURL=change.d.ts.map
@@ -2,13 +2,31 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import { JsonConverter } from '../core/converters/json-converter.js';
4
4
  import { Validator } from '../core/validation/validator.js';
5
+ import { VALIDATION_MESSAGES } from '../core/validation/constants.js';
5
6
  import { ChangeParser } from '../core/parsers/change-parser.js';
6
7
  import { isInteractive } from '../utils/interactive.js';
7
8
  import { getActiveChangeIds } from '../utils/item-discovery.js';
8
- // Constants for better maintainability
9
- const ARCHIVE_DIR = 'archive';
10
- const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
11
- const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
9
+ import { getTaskProgressForChange } from '../utils/task-progress.js';
10
+ import { FileSystemUtils } from '../utils/file-system.js';
11
+ /**
12
+ * True only when `target` is definitively absent. An EACCES or I/O failure
13
+ * means existence cannot be determined, so callers fall through to their
14
+ * read-error path rather than claim the file was never written.
15
+ */
16
+ async function isDefinitelyMissing(target) {
17
+ return fs
18
+ .access(target)
19
+ .then(() => false)
20
+ .catch((error) => error?.code === 'ENOENT');
21
+ }
22
+ /**
23
+ * A change is a directory directly under changes/. Rejecting anything else up
24
+ * front keeps a traversing name (`../..`) from reading a proposal outside the
25
+ * changes directory, and keeps the missing-proposal message honest.
26
+ */
27
+ function isChangeDirectoryName(changesPath, changeDir) {
28
+ return path.dirname(path.resolve(changeDir)) === path.resolve(changesPath);
29
+ }
12
30
  export class ChangeCommand {
13
31
  converter;
14
32
  rootPath;
@@ -31,7 +49,8 @@ export class ChangeCommand {
31
49
  const changesPath = this.getChangesPath();
32
50
  if (!changeName) {
33
51
  const canPrompt = isInteractive(options);
34
- const changes = await this.getActiveChanges(changesPath);
52
+ // Offer exactly the changes `show <name>` can resolve.
53
+ const changes = await getActiveChangeIds(this.rootPath ?? process.cwd());
35
54
  if (canPrompt && changes.length > 0) {
36
55
  const { select } = await import('@inquirer/prompts');
37
56
  const selected = await select({
@@ -52,19 +71,40 @@ export class ChangeCommand {
52
71
  return;
53
72
  }
54
73
  }
55
- const proposalPath = path.join(changesPath, changeName, 'proposal.md');
74
+ const changeDir = path.join(changesPath, changeName);
75
+ const proposalPath = path.join(changeDir, 'proposal.md');
76
+ if (!isChangeDirectoryName(changesPath, changeDir)) {
77
+ throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
78
+ }
56
79
  try {
57
80
  await fs.access(proposalPath);
58
81
  }
59
82
  catch {
83
+ // A change can exist without a proposal: `openspec new change` scaffolds
84
+ // only .openspec.yaml, and a custom schema need not define a proposal
85
+ // artifact. Say which of the two cases this is instead of reporting a
86
+ // change that does exist as missing. A stray file under changes/ is not a
87
+ // change, and naming it one would point the user at a `status --change`
88
+ // call that cannot work.
89
+ const isChangeDirectory = await fs
90
+ .stat(changeDir)
91
+ .then((stats) => stats.isDirectory())
92
+ .catch(() => false);
93
+ if (isChangeDirectory) {
94
+ throw new Error(`Change "${changeName}" has no proposal.md yet. ` +
95
+ `Run "openspec status --change ${changeName}" to see which artifact comes next.`);
96
+ }
60
97
  throw new Error(`Change "${changeName}" not found at ${proposalPath}`);
61
98
  }
99
+ FileSystemUtils.assertPathWithin(path.dirname(proposalPath), proposalPath);
62
100
  if (options?.json) {
101
+ FileSystemUtils.assertPathWithin(changeDir, proposalPath);
63
102
  const jsonOutput = await this.converter.convertChangeToJson(proposalPath);
64
103
  if (options.requirementsOnly) {
65
104
  console.error('Flag --requirements-only is deprecated; use --deltas-only instead.');
66
105
  }
67
106
  const parsed = JSON.parse(jsonOutput);
107
+ FileSystemUtils.assertPathWithin(changeDir, proposalPath);
68
108
  const contentForTitle = await fs.readFile(proposalPath, 'utf-8');
69
109
  const title = this.extractTitle(contentForTitle, changeName);
70
110
  const id = parsed.name;
@@ -79,6 +119,7 @@ export class ChangeCommand {
79
119
  console.log(JSON.stringify(output, null, 2));
80
120
  }
81
121
  else {
122
+ FileSystemUtils.assertPathWithin(changeDir, proposalPath);
82
123
  const content = await fs.readFile(proposalPath, 'utf-8');
83
124
  console.log(content);
84
125
  }
@@ -90,27 +131,31 @@ export class ChangeCommand {
90
131
  */
91
132
  async list(options) {
92
133
  const changesPath = path.join(process.cwd(), 'openspec', 'changes');
93
- const changes = await this.getActiveChanges(changesPath);
134
+ // Same directory-based resolution as `openspec list`, the command this
135
+ // deprecated alias points users at. Every output path below already
136
+ // tolerates a change whose proposal.md is missing or unreadable.
137
+ const changes = await getActiveChangeIds();
94
138
  if (options?.json) {
95
139
  const changeDetails = await Promise.all(changes.map(async (changeName) => {
96
- const proposalPath = path.join(changesPath, changeName, 'proposal.md');
97
- const tasksPath = path.join(changesPath, changeName, 'tasks.md');
140
+ const changeDir = path.join(changesPath, changeName);
141
+ const proposalPath = path.join(changeDir, 'proposal.md');
142
+ // Resolve task progress through the shared tracked-tasks helper so
143
+ // this deprecated noun-form list cannot re-fork the resolution
144
+ // (#1202). Tasks are independent of the proposal: a change can carry
145
+ // tasks before, or without, a proposal.md.
146
+ const taskStatus = await getTaskProgressForChange(changesPath, changeName, process.cwd());
147
+ // No proposal yet is an ordinary state (scaffolded change, or a
148
+ // schema with no proposal artifact), so name the change rather than
149
+ // labelling it Unknown. Unknown stays for a proposal that exists but
150
+ // cannot be read or parsed.
151
+ if (await isDefinitelyMissing(proposalPath)) {
152
+ return { id: changeName, title: changeName, deltaCount: 0, taskStatus };
153
+ }
98
154
  try {
155
+ FileSystemUtils.assertPathWithin(changeDir, proposalPath);
99
156
  const content = await fs.readFile(proposalPath, 'utf-8');
100
- const changeDir = path.join(changesPath, changeName);
101
157
  const parser = new ChangeParser(content, changeDir);
102
158
  const change = await parser.parseChangeWithDeltas(changeName);
103
- let taskStatus = { total: 0, completed: 0 };
104
- try {
105
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
106
- taskStatus = this.countTasks(tasksContent);
107
- }
108
- catch (error) {
109
- // Tasks file may not exist, which is okay
110
- if (process.env.DEBUG) {
111
- console.error(`Failed to read tasks file at ${tasksPath}:`, error);
112
- }
113
- }
114
159
  return {
115
160
  id: changeName,
116
161
  title: this.extractTitle(content, changeName),
@@ -118,13 +163,8 @@ export class ChangeCommand {
118
163
  taskStatus,
119
164
  };
120
165
  }
121
- catch (error) {
122
- return {
123
- id: changeName,
124
- title: 'Unknown',
125
- deltaCount: 0,
126
- taskStatus: { total: 0, completed: 0 },
127
- };
166
+ catch {
167
+ return { id: changeName, title: 'Unknown', deltaCount: 0, taskStatus };
128
168
  }
129
169
  }));
130
170
  const sorted = changeDetails.sort((a, b) => a.id.localeCompare(b.id));
@@ -143,30 +183,25 @@ export class ChangeCommand {
143
183
  }
144
184
  // Long format: id: title and minimal counts
145
185
  for (const changeName of sorted) {
146
- const proposalPath = path.join(changesPath, changeName, 'proposal.md');
147
- const tasksPath = path.join(changesPath, changeName, 'tasks.md');
186
+ const changeDir = path.join(changesPath, changeName);
187
+ const proposalPath = path.join(changeDir, 'proposal.md');
188
+ const { total, completed } = await getTaskProgressForChange(changesPath, changeName, process.cwd());
189
+ const taskStatusText = total > 0 ? ` [tasks ${completed}/${total}]` : '';
190
+ if (await isDefinitelyMissing(proposalPath)) {
191
+ console.log(`${changeName}: (no proposal.md yet)${taskStatusText}`);
192
+ continue;
193
+ }
148
194
  try {
195
+ FileSystemUtils.assertPathWithin(changeDir, proposalPath);
149
196
  const content = await fs.readFile(proposalPath, 'utf-8');
150
197
  const title = this.extractTitle(content, changeName);
151
- let taskStatusText = '';
152
- try {
153
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
154
- const { total, completed } = this.countTasks(tasksContent);
155
- taskStatusText = ` [tasks ${completed}/${total}]`;
156
- }
157
- catch (error) {
158
- if (process.env.DEBUG) {
159
- console.error(`Failed to read tasks file at ${tasksPath}:`, error);
160
- }
161
- }
162
- const changeDir = path.join(changesPath, changeName);
163
- const parser = new ChangeParser(await fs.readFile(proposalPath, 'utf-8'), changeDir);
198
+ const parser = new ChangeParser(content, changeDir);
164
199
  const change = await parser.parseChangeWithDeltas(changeName);
165
200
  const deltaCountText = ` [deltas ${change.deltas.length}]`;
166
201
  console.log(`${changeName}: ${title}${deltaCountText}${taskStatusText}`);
167
202
  }
168
203
  catch {
169
- console.log(`${changeName}: (unable to read)`);
204
+ console.log(`${changeName}: (unable to read)${taskStatusText}`);
170
205
  }
171
206
  }
172
207
  }
@@ -197,6 +232,9 @@ export class ChangeCommand {
197
232
  }
198
233
  }
199
234
  const changeDir = path.join(changesPath, changeName);
235
+ if (!isChangeDirectoryName(changesPath, changeDir)) {
236
+ throw new Error(`Change "${changeName}" not found at ${changeDir}`);
237
+ }
200
238
  try {
201
239
  await fs.access(changeDir);
202
240
  }
@@ -204,7 +242,12 @@ export class ChangeCommand {
204
242
  throw new Error(`Change "${changeName}" not found at ${changeDir}`);
205
243
  }
206
244
  const validator = new Validator(options?.strict || false);
207
- const report = await validator.validateChangeDeltaSpecs(changeDir);
245
+ const report = await validator.validateChangeDeltaSpecs(changeDir, {
246
+ // Derived from changesPath so the main specs come from the same root the
247
+ // change itself was resolved against.
248
+ mainSpecsDir: path.join(path.dirname(changesPath), 'specs'),
249
+ projectRoot: path.dirname(path.dirname(changesPath)),
250
+ });
208
251
  if (options?.json) {
209
252
  console.log(JSON.stringify(report, null, 2));
210
253
  }
@@ -220,58 +263,36 @@ export class ChangeCommand {
220
263
  console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
221
264
  });
222
265
  // Next steps footer to guide fixing issues
223
- this.printNextSteps();
266
+ this.printNextSteps(report.issues);
224
267
  if (!options?.json) {
225
268
  process.exitCode = 1;
226
269
  }
227
270
  }
228
271
  }
229
272
  }
230
- async getActiveChanges(changesPath) {
231
- try {
232
- const entries = await fs.readdir(changesPath, { withFileTypes: true });
233
- const result = [];
234
- for (const entry of entries) {
235
- if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === ARCHIVE_DIR)
236
- continue;
237
- const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
238
- try {
239
- await fs.access(proposalPath);
240
- result.push(entry.name);
241
- }
242
- catch {
243
- // skip directories without proposal.md
244
- }
245
- }
246
- return result.sort();
247
- }
248
- catch {
249
- return [];
250
- }
251
- }
252
273
  extractTitle(content, changeName) {
253
274
  const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/im);
254
275
  return match ? match[1].trim() : changeName;
255
276
  }
256
- countTasks(content) {
257
- const lines = content.split('\n');
258
- let total = 0;
259
- let completed = 0;
260
- for (const line of lines) {
261
- if (line.match(TASK_PATTERN)) {
262
- total++;
263
- if (line.match(COMPLETED_TASK_PATTERN)) {
264
- completed++;
265
- }
266
- }
267
- }
268
- return { total, completed };
269
- }
270
- printNextSteps() {
277
+ printNextSteps(issues = []) {
271
278
  const bullets = [];
272
- bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
273
- bullets.push('- Each requirement MUST include at least one #### Scenario: block');
274
- bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
279
+ // Branch on the exact marker messages: the generic no-deltas guidance
280
+ // also mentions skip_specs and must not trigger the marker bullets.
281
+ const conflictIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT));
282
+ const invalidMarkerIssue = issues.some(i => i.message.includes(VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA));
283
+ if (conflictIssue) {
284
+ bullets.push('- This change declares skip_specs (no spec deltas): delete the files under specs/, or remove skip_specs from .openspec.yaml if requirements do change');
285
+ bullets.push('- skip_specs is only honored when .openspec.yaml is valid change metadata (schema: <name> is required)');
286
+ }
287
+ else if (invalidMarkerIssue) {
288
+ bullets.push('- Fix .openspec.yaml so the skip_specs marker can be honored (schema: <name> is required)');
289
+ bullets.push('- Or remove skip_specs from .openspec.yaml and add delta specs instead');
290
+ }
291
+ else {
292
+ bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
293
+ bullets.push('- Each requirement MUST include at least one #### Scenario: block');
294
+ bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
295
+ }
275
296
  console.error('Next steps:');
276
297
  bullets.forEach(b => console.error(` ${b}`));
277
298
  }
@@ -1,13 +1,13 @@
1
- import { spawn, execSync } from 'node:child_process';
2
- import { trackEvent } from '../telemetry/index.js';
1
+ import { spawn } from 'node:child_process';
3
2
  import * as fs from 'node:fs';
4
3
  import * as path from 'node:path';
5
4
  import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
6
- import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
5
+ import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
7
6
  import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
8
7
  import { OPENSPEC_DIR_NAME } from '../core/config.js';
9
8
  import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
10
- import { isPromptCancellationError } from './shared-output.js';
9
+ import { UpdateCommand } from '../core/update.js';
10
+ import { asErrorMessage, isPromptCancellationError } from './shared-output.js';
11
11
  const WORKFLOW_PROMPT_META = {
12
12
  propose: {
13
13
  name: 'Propose change',
@@ -234,11 +234,15 @@ export function registerConfigCommand(program) {
234
234
  .action((key, value, options) => {
235
235
  const allowUnknown = Boolean(options.allowUnknown);
236
236
  const keyValidation = validateConfigKeyPath(key);
237
- if (!keyValidation.valid && !allowUnknown) {
237
+ // --allow-unknown relaxes the known-key check, but never the prototype-safety check.
238
+ const unsafeKey = hasUnsafeKeySegment(key);
239
+ if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
238
240
  const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
239
241
  console.error(`Error: Invalid configuration key "${key}".${reason}`);
240
242
  console.error('Use "openspec config list" to see available keys.');
241
- console.error('Pass --allow-unknown to bypass this check.');
243
+ if (!allowUnknown && !unsafeKey) {
244
+ console.error('Pass --allow-unknown to bypass this check.');
245
+ }
242
246
  process.exitCode = 1;
243
247
  return;
244
248
  }
@@ -257,7 +261,6 @@ export function registerConfigCommand(program) {
257
261
  // Apply changes and save
258
262
  setNestedValue(config, key, coercedValue);
259
263
  saveGlobalConfig(config);
260
- trackEvent('config_value_set', { key });
261
264
  const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
262
265
  console.log(`Set ${key} = ${displayValue}`);
263
266
  });
@@ -519,12 +522,6 @@ export function registerConfigCommand(program) {
519
522
  config.delivery = nextState.delivery;
520
523
  config.workflows = nextState.workflows;
521
524
  saveGlobalConfig(config);
522
- trackEvent('config_profile_changed', {
523
- profile: nextState.profile,
524
- delivery: nextState.delivery,
525
- workflow_count: nextState.workflows.length,
526
- changes: diff.lines,
527
- });
528
525
  // Check if inside an OpenSpec project
529
526
  const projectDir = process.cwd();
530
527
  const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
@@ -535,11 +532,12 @@ export function registerConfigCommand(program) {
535
532
  });
536
533
  if (applyNow) {
537
534
  try {
538
- execSync('npx openspec update', { stdio: 'inherit', cwd: projectDir });
535
+ await new UpdateCommand().execute(projectDir);
539
536
  console.log('Run `openspec update` in your other projects to apply.');
540
537
  }
541
- catch {
542
- console.error('`openspec update` failed. Please run it manually to apply the profile changes.');
538
+ catch (error) {
539
+ console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
540
+ console.error('Please run it manually to apply the profile changes.');
543
541
  process.exitCode = 1;
544
542
  }
545
543
  return;