@codewalla_india/openspec 1.3.0 → 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 -230
  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 -323
  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 -71
  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
@@ -2,8 +2,9 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import ora from 'ora';
4
4
  import { stringify as stringifyYaml } from 'yaml';
5
- import { getSchemaDir, getProjectSchemasDir, getUserSchemasDir, getPackageSchemasDir, listSchemas, } from '../core/artifact-graph/resolver.js';
5
+ import { getSchemaDir, getProjectSchemasDir, getUserSchemasDir, getPackageSchemasDir, isSchemaDir, listSchemas, } from '../core/artifact-graph/resolver.js';
6
6
  import { parseSchema, SchemaValidationError } from '../core/artifact-graph/schema.js';
7
+ import { FileSystemUtils } from '../utils/file-system.js';
7
8
  /**
8
9
  * Check all three locations for a schema and return which ones exist.
9
10
  */
@@ -129,21 +130,30 @@ function validateSchema(schemaDir, verbose = false) {
129
130
  }
130
131
  return { valid: false, issues };
131
132
  }
132
- // Check template files exist
133
- // Templates can be in schemaDir directly or in a templates/ subdirectory
133
+ // Check template files exist in the same directory used at runtime.
134
134
  if (verbose) {
135
135
  console.log(' Checking template files...');
136
136
  }
137
137
  for (const artifact of schema.artifacts) {
138
- // Try templates subdirectory first (standard location), then root
139
- const templatePathInTemplates = path.join(schemaDir, 'templates', artifact.template);
140
- const templatePathInRoot = path.join(schemaDir, artifact.template);
141
- if (!fs.existsSync(templatePathInTemplates) && !fs.existsSync(templatePathInRoot)) {
138
+ const templatesDir = path.join(schemaDir, 'templates');
139
+ const existingTemplatePath = path.join(templatesDir, artifact.template);
140
+ if (!fs.existsSync(existingTemplatePath)) {
142
141
  issues.push({
143
142
  level: 'error',
144
143
  path: `artifacts.${artifact.id}.template`,
145
144
  message: `Template file '${artifact.template}' not found for artifact '${artifact.id}'`,
146
145
  });
146
+ continue;
147
+ }
148
+ try {
149
+ FileSystemUtils.assertPathWithin(templatesDir, existingTemplatePath);
150
+ }
151
+ catch {
152
+ issues.push({
153
+ level: 'error',
154
+ path: `artifacts.${artifact.id}.template`,
155
+ message: `Template file '${artifact.template}' points outside the schema templates directory`,
156
+ });
147
157
  }
148
158
  }
149
159
  // Dependency graph validation is already done by parseSchema
@@ -162,19 +172,73 @@ function isValidSchemaName(name) {
162
172
  /**
163
173
  * Copy a directory recursively.
164
174
  */
165
- function copyDirRecursive(src, dest) {
175
+ function resolveSchemaCopyPath(allowedRoot, sourcePath) {
176
+ try {
177
+ const canonicalRoot = fs.realpathSync(allowedRoot);
178
+ const canonicalPath = fs.realpathSync(sourcePath);
179
+ FileSystemUtils.assertPathWithin(canonicalRoot, canonicalPath);
180
+ return canonicalPath;
181
+ }
182
+ catch (error) {
183
+ const detail = error instanceof Error ? error.message : String(error);
184
+ throw new Error(`Cannot fork schema with linked or unsupported entry: ${sourcePath}: ${detail}`, { cause: error });
185
+ }
186
+ }
187
+ function copyDirRecursive(src, dest, allowedRoot = src, ancestors = new Set()) {
188
+ const canonicalSrc = resolveSchemaCopyPath(allowedRoot, src);
189
+ if (ancestors.has(canonicalSrc)) {
190
+ throw new Error(`Cannot fork schema with a linked directory cycle: ${src}`);
191
+ }
192
+ ancestors.add(canonicalSrc);
166
193
  fs.mkdirSync(dest, { recursive: true });
167
- const entries = fs.readdirSync(src, { withFileTypes: true });
168
- for (const entry of entries) {
169
- const srcPath = path.join(src, entry.name);
170
- const destPath = path.join(dest, entry.name);
171
- if (entry.isDirectory()) {
172
- copyDirRecursive(srcPath, destPath);
194
+ try {
195
+ const entries = fs.readdirSync(src, { withFileTypes: true });
196
+ for (const entry of entries) {
197
+ const srcPath = path.join(src, entry.name);
198
+ const destPath = path.join(dest, entry.name);
199
+ const canonicalEntry = resolveSchemaCopyPath(allowedRoot, srcPath);
200
+ const stats = fs.statSync(canonicalEntry);
201
+ if (stats.isDirectory()) {
202
+ copyDirRecursive(canonicalEntry, destPath, allowedRoot, ancestors);
203
+ }
204
+ else if (stats.isFile()) {
205
+ // Dereference confined links so the fork is an independent schema.
206
+ fs.copyFileSync(canonicalEntry, destPath);
207
+ }
208
+ else {
209
+ throw new Error(`Cannot fork schema with linked or unsupported entry: ${srcPath}`);
210
+ }
173
211
  }
174
- else {
175
- fs.copyFileSync(srcPath, destPath);
212
+ }
213
+ finally {
214
+ ancestors.delete(canonicalSrc);
215
+ }
216
+ }
217
+ /**
218
+ * Verifies a schema tree before replacing or creating the fork destination.
219
+ */
220
+ function assertSchemaTreeCanBeCopied(src, allowedRoot = src, ancestors = new Set()) {
221
+ const canonicalSrc = resolveSchemaCopyPath(allowedRoot, src);
222
+ if (ancestors.has(canonicalSrc)) {
223
+ throw new Error(`Cannot fork schema with a linked directory cycle: ${src}`);
224
+ }
225
+ ancestors.add(canonicalSrc);
226
+ try {
227
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
228
+ const entryPath = path.join(src, entry.name);
229
+ const canonicalEntry = resolveSchemaCopyPath(allowedRoot, entryPath);
230
+ const stats = fs.statSync(canonicalEntry);
231
+ if (stats.isDirectory()) {
232
+ assertSchemaTreeCanBeCopied(canonicalEntry, allowedRoot, ancestors);
233
+ }
234
+ else if (!stats.isFile()) {
235
+ throw new Error(`Cannot fork schema with linked or unsupported entry: ${entryPath}`);
236
+ }
176
237
  }
177
238
  }
239
+ finally {
240
+ ancestors.delete(canonicalSrc);
241
+ }
178
242
  }
179
243
  /**
180
244
  * Default artifacts with descriptions for schema init.
@@ -339,7 +403,7 @@ export function registerSchemaCommand(program) {
339
403
  const schemaResults = [];
340
404
  let anyInvalid = false;
341
405
  for (const entry of entries) {
342
- if (!entry.isDirectory())
406
+ if (!isSchemaDir(projectSchemasDir, entry))
343
407
  continue;
344
408
  const schemaDir = path.join(projectSchemasDir, entry.name);
345
409
  const schemaPath = path.join(schemaDir, 'schema.yaml');
@@ -378,9 +442,9 @@ export function registerSchemaCommand(program) {
378
442
  console.log(` ${issue.level}: ${issue.message}`);
379
443
  }
380
444
  }
381
- if (anyInvalid) {
382
- process.exitCode = 1;
383
- }
445
+ }
446
+ if (anyInvalid) {
447
+ process.exitCode = 1;
384
448
  }
385
449
  return;
386
450
  }
@@ -423,9 +487,11 @@ export function registerSchemaCommand(program) {
423
487
  for (const issue of result.issues) {
424
488
  console.log(` ${issue.level}: ${issue.message}`);
425
489
  }
426
- process.exitCode = 1;
427
490
  }
428
491
  }
492
+ if (!result.valid) {
493
+ process.exitCode = 1;
494
+ }
429
495
  }
430
496
  catch (error) {
431
497
  if (options?.json) {
@@ -487,6 +553,9 @@ export function registerSchemaCommand(program) {
487
553
  // Determine source location
488
554
  const sourceResolution = getSchemaResolution(source, projectRoot);
489
555
  const sourceLocation = sourceResolution?.source || 'package';
556
+ // Validate the complete source before a forced fork removes anything.
557
+ const trustedSourceDir = fs.realpathSync(sourceDir);
558
+ assertSchemaTreeCanBeCopied(trustedSourceDir);
490
559
  // Check destination
491
560
  const destinationDir = path.join(getProjectSchemasDir(projectRoot), destinationName);
492
561
  if (fs.existsSync(destinationDir)) {
@@ -513,7 +582,7 @@ export function registerSchemaCommand(program) {
513
582
  // Copy schema
514
583
  if (spinner)
515
584
  spinner.start(`Forking '${source}' to '${destinationName}'...`);
516
- copyDirRecursive(sourceDir, destinationDir);
585
+ copyDirRecursive(trustedSourceDir, destinationDir);
517
586
  // Update name in schema.yaml
518
587
  const destSchemaPath = path.join(destinationDir, 'schema.yaml');
519
588
  const schemaContent = fs.readFileSync(destSchemaPath, 'utf-8');
@@ -584,8 +653,9 @@ export function registerSchemaCommand(program) {
584
653
  return;
585
654
  }
586
655
  const schemaDir = path.join(getProjectSchemasDir(projectRoot), name);
587
- // Check if exists
588
- if (fs.existsSync(schemaDir)) {
656
+ // Check overwrite permission without mutating the destination
657
+ const schemaExists = fs.existsSync(schemaDir);
658
+ if (schemaExists) {
589
659
  if (!options?.force) {
590
660
  if (options?.json) {
591
661
  console.log(JSON.stringify({
@@ -601,9 +671,6 @@ export function registerSchemaCommand(program) {
601
671
  process.exitCode = 1;
602
672
  return;
603
673
  }
604
- if (spinner)
605
- spinner.start(`Removing existing schema '${name}'...`);
606
- fs.rmSync(schemaDir, { recursive: true });
607
674
  }
608
675
  // Determine artifacts and description
609
676
  let description;
@@ -625,6 +692,12 @@ export function registerSchemaCommand(program) {
625
692
  }));
626
693
  selectedArtifactIds = await checkbox({
627
694
  message: 'Select artifacts to include:',
695
+ theme: {
696
+ icon: {
697
+ checked: '[x]',
698
+ unchecked: '[ ]',
699
+ },
700
+ },
628
701
  choices: artifactChoices,
629
702
  });
630
703
  if (selectedArtifactIds.length === 0) {
@@ -673,10 +746,6 @@ export function registerSchemaCommand(program) {
673
746
  selectedArtifactIds = DEFAULT_ARTIFACTS.map((a) => a.id);
674
747
  }
675
748
  }
676
- // Create schema directory
677
- if (spinner)
678
- spinner.start(`Creating schema '${name}'...`);
679
- fs.mkdirSync(schemaDir, { recursive: true });
680
749
  // Build artifacts array with proper dependencies
681
750
  const selectedArtifacts = selectedArtifactIds.map((id) => {
682
751
  const template = DEFAULT_ARTIFACTS.find((a) => a.id === id);
@@ -718,6 +787,16 @@ export function registerSchemaCommand(program) {
718
787
  tracks: 'tasks.md',
719
788
  };
720
789
  }
790
+ // Replace only after all inputs have been collected and validated
791
+ if (schemaExists) {
792
+ if (spinner)
793
+ spinner.start(`Removing existing schema '${name}'...`);
794
+ fs.rmSync(schemaDir, { recursive: true });
795
+ }
796
+ // Create schema directory
797
+ if (spinner)
798
+ spinner.start(`Creating schema '${name}'...`);
799
+ fs.mkdirSync(schemaDir, { recursive: true });
721
800
  fs.writeFileSync(path.join(schemaDir, 'schema.yaml'), stringifyYaml(schema));
722
801
  // Create template files in templates/ subdirectory (standard location)
723
802
  const templatesDir = path.join(schemaDir, 'templates');
@@ -795,74 +874,74 @@ export function registerSchemaCommand(program) {
795
874
  function createDefaultTemplate(artifactId) {
796
875
  switch (artifactId) {
797
876
  case 'proposal':
798
- return `## Why
799
-
800
- <!-- Describe the motivation for this change -->
801
-
802
- ## What Changes
803
-
804
- <!-- Describe what will change -->
805
-
806
- ## Capabilities
807
-
808
- ### New Capabilities
809
- <!-- List new capabilities -->
810
-
811
- ### Modified Capabilities
812
- <!-- List modified capabilities -->
813
-
814
- ## Impact
815
-
816
- <!-- Describe the impact on existing functionality -->
877
+ return `## Why
878
+
879
+ <!-- Describe the motivation for this change -->
880
+
881
+ ## What Changes
882
+
883
+ <!-- Describe what will change -->
884
+
885
+ ## Capabilities
886
+
887
+ ### New Capabilities
888
+ <!-- List new capabilities -->
889
+
890
+ ### Modified Capabilities
891
+ <!-- List modified capabilities -->
892
+
893
+ ## Impact
894
+
895
+ <!-- Describe the impact on existing functionality -->
817
896
  `;
818
897
  case 'specs':
819
- return `## ADDED Requirements
820
-
821
- ### Requirement: Example requirement
822
-
823
- Description of the requirement.
824
-
825
- #### Scenario: Example scenario
826
- - **WHEN** some condition
827
- - **THEN** some outcome
898
+ return `## ADDED Requirements
899
+
900
+ ### Requirement: Example requirement
901
+
902
+ Description of the requirement.
903
+
904
+ #### Scenario: Example scenario
905
+ - **WHEN** some condition
906
+ - **THEN** some outcome
828
907
  `;
829
908
  case 'design':
830
- return `## Context
831
-
832
- <!-- Background and context -->
833
-
834
- ## Goals / Non-Goals
835
-
836
- **Goals:**
837
- <!-- List goals -->
838
-
839
- **Non-Goals:**
840
- <!-- List non-goals -->
841
-
842
- ## Decisions
843
-
844
- ### 1. Decision Name
845
-
846
- Description and rationale.
847
-
848
- **Alternatives considered:**
849
- - Alternative 1: Rejected because...
850
-
851
- ## Risks / Trade-offs
852
-
853
- <!-- List risks and trade-offs -->
909
+ return `## Context
910
+
911
+ <!-- Background and context -->
912
+
913
+ ## Goals / Non-Goals
914
+
915
+ **Goals:**
916
+ <!-- List goals -->
917
+
918
+ **Non-Goals:**
919
+ <!-- List non-goals -->
920
+
921
+ ## Decisions
922
+
923
+ ### 1. Decision Name
924
+
925
+ Description and rationale.
926
+
927
+ **Alternatives considered:**
928
+ - Alternative 1: Rejected because...
929
+
930
+ ## Risks / Trade-offs
931
+
932
+ <!-- List risks and trade-offs -->
854
933
  `;
855
934
  case 'tasks':
856
- return `## Implementation Tasks
857
-
858
- - [ ] Task 1
859
- - [ ] Task 2
860
- - [ ] Task 3
935
+ return `## Implementation Tasks
936
+
937
+ - [ ] Task 1
938
+ - [ ] Task 2
939
+ - [ ] Task 3
861
940
  `;
862
941
  default:
863
- return `## ${artifactId}
864
-
865
- <!-- Add content here -->
942
+ return `## ${artifactId}
943
+
944
+ <!-- Add content here -->
866
945
  `;
867
946
  }
868
947
  }
@@ -157,14 +157,17 @@ export class ShowCommand {
157
157
  }
158
158
  warnIrrelevantFlags(type, options) {
159
159
  const irrelevant = [];
160
+ // --no-scenarios makes commander default `scenarios` to true, so its
161
+ // presence alone does not mean the user passed it — only false does.
162
+ const isUserProvided = (k) => k === 'scenarios' ? options[k] === false : k in options;
160
163
  if (type === 'change') {
161
164
  for (const k of SPEC_FLAG_KEYS)
162
- if (k in options)
165
+ if (isUserProvided(k))
163
166
  irrelevant.push(k);
164
167
  }
165
168
  else {
166
169
  for (const k of CHANGE_FLAG_KEYS)
167
- if (k in options)
170
+ if (isUserProvided(k))
168
171
  irrelevant.push(k);
169
172
  }
170
173
  if (irrelevant.length > 0) {
@@ -1,11 +1,31 @@
1
- import { existsSync, readdirSync, readFileSync } from 'fs';
2
- import { join } from 'path';
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import path, { join } from 'path';
3
3
  import { MarkdownParser } from '../core/parsers/markdown-parser.js';
4
4
  import { Validator } from '../core/validation/validator.js';
5
5
  import { isInteractive } from '../utils/interactive.js';
6
6
  import { getSpecIds } from '../utils/item-discovery.js';
7
+ import { discoverSpecFiles } from '../utils/spec-discovery.js';
8
+ import { FileSystemUtils } from '../utils/file-system.js';
7
9
  const SPECS_DIR = 'openspec/specs';
8
- function parseSpecFromFile(specPath, specId) {
10
+ function assertSpecPath(specsDir, specPath) {
11
+ const relativePath = path.relative(path.resolve(specsDir), path.resolve(specPath));
12
+ if (relativePath === '..' ||
13
+ relativePath.startsWith(`..${path.sep}`) ||
14
+ path.isAbsolute(relativePath)) {
15
+ throw new Error(`Path is outside the allowed directory: ${specPath}`);
16
+ }
17
+ try {
18
+ // Preserve confined spec.md links, including links to a sibling capability.
19
+ FileSystemUtils.assertPathWithin(specsDir, specPath);
20
+ }
21
+ catch {
22
+ // A capability directory may intentionally be a monorepo symlink. Treat it
23
+ // as the trust root while still rejecting a link outside that capability.
24
+ FileSystemUtils.assertPathWithin(path.dirname(specPath), specPath);
25
+ }
26
+ }
27
+ function parseSpecFromFile(specsDir, specPath, specId) {
28
+ assertSpecPath(specsDir, specPath);
9
29
  const content = readFileSync(specPath, 'utf-8');
10
30
  const parser = new MarkdownParser(content);
11
31
  return parser.parseSpec(specId);
@@ -40,7 +60,8 @@ function filterSpec(spec, options) {
40
60
  * Print the raw markdown content for a spec file without any formatting.
41
61
  * Raw-first behavior ensures text mode is a passthrough for deterministic output.
42
62
  */
43
- function printSpecTextRaw(specPath) {
63
+ function printSpecTextRaw(specsDir, specPath) {
64
+ assertSpecPath(specsDir, specPath);
44
65
  const content = readFileSync(specPath, 'utf-8');
45
66
  console.log(content);
46
67
  }
@@ -69,6 +90,7 @@ export class SpecCommand {
69
90
  }
70
91
  }
71
92
  const specPath = join(this.specsDir, specId, 'spec.md');
93
+ assertSpecPath(this.specsDir, specPath);
72
94
  if (!existsSync(specPath)) {
73
95
  // Root-aware callers get the absolute path; the cwd-based noun form
74
96
  // keeps its historical forward-slash relative message on all platforms.
@@ -79,7 +101,7 @@ export class SpecCommand {
79
101
  if (options.requirements && options.requirement) {
80
102
  throw new Error('Options --requirements and --requirement cannot be used together');
81
103
  }
82
- const parsed = parseSpecFromFile(specPath, specId);
104
+ const parsed = parseSpecFromFile(this.specsDir, specPath, specId);
83
105
  const filtered = filterSpec(parsed, options);
84
106
  const output = {
85
107
  id: specId,
@@ -93,7 +115,7 @@ export class SpecCommand {
93
115
  console.log(JSON.stringify(output, null, 2));
94
116
  return;
95
117
  }
96
- printSpecTextRaw(specPath);
118
+ printSpecTextRaw(this.specsDir, specPath);
97
119
  }
98
120
  }
99
121
  export function registerSpecCommand(rootProgram) {
@@ -127,36 +149,32 @@ export function registerSpecCommand(rootProgram) {
127
149
  .description('List all available specifications')
128
150
  .option('--json', 'Output as JSON')
129
151
  .option('--long', 'Show id and title with counts')
130
- .action((options) => {
152
+ .action(async (options) => {
131
153
  try {
132
154
  if (!existsSync(SPECS_DIR)) {
133
155
  console.log('No items found');
134
156
  return;
135
157
  }
136
- const specs = readdirSync(SPECS_DIR, { withFileTypes: true })
137
- .filter(dirent => dirent.isDirectory())
138
- .map(dirent => {
139
- const specPath = join(SPECS_DIR, dirent.name, 'spec.md');
140
- if (existsSync(specPath)) {
141
- try {
142
- const spec = parseSpecFromFile(specPath, dirent.name);
143
- return {
144
- id: dirent.name,
145
- title: spec.name,
146
- requirementCount: spec.requirements.length
147
- };
148
- }
149
- catch {
150
- return {
151
- id: dirent.name,
152
- title: dirent.name,
153
- requirementCount: 0
154
- };
155
- }
158
+ const discovered = await discoverSpecFiles(SPECS_DIR);
159
+ const specs = discovered
160
+ .map(({ id, specFile }) => {
161
+ try {
162
+ assertSpecPath(SPECS_DIR, specFile);
163
+ const spec = parseSpecFromFile(SPECS_DIR, specFile, id);
164
+ return {
165
+ id,
166
+ title: spec.name,
167
+ requirementCount: spec.requirements.length
168
+ };
169
+ }
170
+ catch {
171
+ return {
172
+ id,
173
+ title: id,
174
+ requirementCount: 0
175
+ };
156
176
  }
157
- return null;
158
177
  })
159
- .filter((spec) => spec !== null)
160
178
  .sort((a, b) => a.id.localeCompare(b.id));
161
179
  if (options.json) {
162
180
  console.log(JSON.stringify(specs, null, 2));
@@ -203,10 +221,12 @@ export function registerSpecCommand(rootProgram) {
203
221
  }
204
222
  }
205
223
  const specPath = join(SPECS_DIR, specId, 'spec.md');
224
+ assertSpecPath(SPECS_DIR, specPath);
206
225
  if (!existsSync(specPath)) {
207
226
  throw new Error(`Spec '${specId}' not found at openspec/specs/${specId}/spec.md`);
208
227
  }
209
228
  const validator = new Validator(options.strict);
229
+ assertSpecPath(SPECS_DIR, specPath);
210
230
  const report = await validator.validateSpec(specPath);
211
231
  if (options.json) {
212
232
  console.log(JSON.stringify(report, null, 2));
@@ -1,10 +1,10 @@
1
1
  import * as os from 'node:os';
2
2
  import { asErrorMessage, emitFailure, printJson } from './shared-output.js';
3
3
  import * as path from 'node:path';
4
- import { trackEvent, trackCommandFailed } from '../telemetry/index.js';
5
4
  import { COMMAND_REGISTRY } from '../core/completions/command-registry.js';
6
5
  import { StoreError, doctorStores, listStores, prepareStoreSetup, prepareStoreCleanup, registerExistingStore, removeStore, resolveSetupGitEnabled, setupPreparedStore, unregisterStore, validateStoreId, } from '../core/store/index.js';
7
6
  import { isInteractive } from '../utils/interactive.js';
7
+ import { trackStoreOperation } from '../telemetry/index.js';
8
8
  function toStoreOutput(store) {
9
9
  return {
10
10
  id: store.id,
@@ -327,19 +327,15 @@ class StoreCommand {
327
327
  }
328
328
  const result = await setupPreparedStore(prepared, { initGit });
329
329
  const payload = toMutationOutput(result);
330
- trackEvent('store_setup', {
331
- git_initialized: result.git.initialized,
332
- already_registered: result.registryCommit.alreadyRegistered,
333
- has_remote: Boolean(options.remote),
334
- });
335
330
  if (options.json) {
336
331
  printJson(payload);
337
332
  return;
338
333
  }
339
334
  printMutationHuman('Store ready', payload, result.remotes);
335
+ // Track store setup operation
336
+ await trackStoreOperation('register', result.store?.id);
340
337
  }
341
338
  catch (error) {
342
- await trackCommandFailed('store_setup', error);
343
339
  this.handleFailure(options.json, { store: null, registry: null, git: null, created_files: [], status: [] }, error);
344
340
  }
345
341
  }
@@ -365,18 +361,15 @@ class StoreCommand {
365
361
  });
366
362
  }
367
363
  const payload = toMutationOutput(result);
368
- trackEvent('store_registered', {
369
- already_registered: result.registryCommit.alreadyRegistered,
370
- git_is_repository: result.git.isRepository,
371
- });
372
364
  if (options.json) {
373
365
  printJson(payload);
374
366
  return;
375
367
  }
376
368
  printMutationHuman('Store registered', payload, result.remotes);
369
+ // Track store register operation
370
+ await trackStoreOperation('register', result.store?.id);
377
371
  }
378
372
  catch (error) {
379
- await trackCommandFailed('store_register', error);
380
373
  this.handleFailure(options.json, { store: null, registry: null, git: null, created_files: [], status: [] }, error);
381
374
  }
382
375
  }
@@ -388,6 +381,8 @@ class StoreCommand {
388
381
  return;
389
382
  }
390
383
  printCleanupHuman('Unregistered store', payload);
384
+ // Track store unregister operation
385
+ await trackStoreOperation('unregister', id);
391
386
  }
392
387
  catch (error) {
393
388
  this.handleFailure(options.json, { store: null, registry: null, files: null, status: [] }, error);
@@ -397,30 +392,29 @@ class StoreCommand {
397
392
  try {
398
393
  const target = await prepareStoreCleanup({ id });
399
394
  await confirmRemove(target.id, target.root, options);
400
- const result = await removeStore(target);
401
- const payload = toCleanupOutput(result);
402
- trackEvent('store_removed', {
403
- files_deleted: result.files.deleted,
404
- });
395
+ const payload = toCleanupOutput(await removeStore(target));
405
396
  if (options.json) {
406
397
  printJson(payload);
407
398
  return;
408
399
  }
409
400
  printCleanupHuman('Removed store', payload);
401
+ // Track store remove operation
402
+ await trackStoreOperation('remove', id);
410
403
  }
411
404
  catch (error) {
412
- await trackCommandFailed('store_remove', error);
413
405
  this.handleFailure(options.json, { store: null, registry: null, files: null, status: [] }, error);
414
406
  }
415
407
  }
416
408
  async list(options = {}) {
417
409
  try {
418
- const payload = toListOutput(await listStores());
410
+ const result = await listStores();
419
411
  if (options.json) {
420
- printJson(payload);
412
+ printJson({ stores: result.stores, status: [] });
421
413
  return;
422
414
  }
423
- printListHuman(payload);
415
+ printListHuman({ stores: result.stores, status: [] });
416
+ // Track store list operation
417
+ await trackStoreOperation('list');
424
418
  }
425
419
  catch (error) {
426
420
  this.handleFailure(options.json, { stores: [], status: [] }, error);
@@ -14,6 +14,14 @@ interface ExecuteOptions {
14
14
  export declare class ValidateCommand {
15
15
  execute(itemName: string | undefined, options?: ExecuteOptions): Promise<void>;
16
16
  private normalizeType;
17
+ /**
18
+ * Resolve change IDs by directory existence within the resolved root — the
19
+ * same rule `openspec status`/`instructions` use (`getAvailableChanges`) —
20
+ * rather than requiring `proposal.md`. This lets `validate` resolve a
21
+ * scaffolded or still-authoring change that the sibling commands already
22
+ * resolve (#1182). Sorted to preserve the prior `getActiveChangeIds` ordering.
23
+ */
24
+ private listChangeIds;
17
25
  private runInteractiveSelector;
18
26
  private printNonInteractiveHint;
19
27
  private validateDirectItem;