@codewalla_india/openspec 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +214 -133
  3. package/bin/openspec.js +5 -5
  4. package/dist/cli/index.js +99 -62
  5. package/dist/commands/change.d.ts +0 -2
  6. package/dist/commands/change.js +107 -86
  7. package/dist/commands/config.js +14 -16
  8. package/dist/commands/doctor.js +9 -4
  9. package/dist/commands/feedback.js +79 -30
  10. package/dist/commands/modify.d.ts +26 -0
  11. package/dist/commands/modify.js +147 -0
  12. package/dist/commands/schema.js +170 -91
  13. package/dist/commands/show.js +5 -2
  14. package/dist/commands/spec.js +49 -29
  15. package/dist/commands/store.js +15 -21
  16. package/dist/commands/validate.d.ts +8 -0
  17. package/dist/commands/validate.js +43 -20
  18. package/dist/commands/workflow/index.d.ts +2 -2
  19. package/dist/commands/workflow/index.js +1 -1
  20. package/dist/commands/workflow/instructions.d.ts +6 -26
  21. package/dist/commands/workflow/instructions.js +183 -376
  22. package/dist/commands/workflow/new-change.d.ts +0 -4
  23. package/dist/commands/workflow/new-change.js +10 -28
  24. package/dist/commands/workflow/shared.d.ts +34 -19
  25. package/dist/commands/workflow/shared.js +34 -4
  26. package/dist/commands/workflow/status.js +10 -32
  27. package/dist/commands/workflow/templates.js +15 -5
  28. package/dist/commands/workset.d.ts +2 -2
  29. package/dist/commands/workset.js +19 -22
  30. package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
  31. package/dist/comprehension-quiz/fingerprint.js +84 -0
  32. package/dist/comprehension-quiz/index.d.ts +42 -0
  33. package/dist/comprehension-quiz/index.js +47 -0
  34. package/dist/comprehension-quiz/pass-record.d.ts +44 -0
  35. package/dist/comprehension-quiz/pass-record.js +98 -0
  36. package/dist/comprehension-quiz/providers/anthropic.d.ts +35 -0
  37. package/dist/comprehension-quiz/providers/anthropic.js +69 -0
  38. package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
  39. package/dist/comprehension-quiz/providers/factory.js +174 -0
  40. package/dist/comprehension-quiz/providers/interface.d.ts +98 -0
  41. package/dist/comprehension-quiz/providers/interface.js +150 -0
  42. package/dist/comprehension-quiz/providers/local.d.ts +35 -0
  43. package/dist/comprehension-quiz/providers/local.js +69 -0
  44. package/dist/comprehension-quiz/providers/ollama.d.ts +35 -0
  45. package/dist/comprehension-quiz/providers/ollama.js +69 -0
  46. package/dist/comprehension-quiz/providers/openai.d.ts +35 -0
  47. package/dist/comprehension-quiz/providers/openai.js +72 -0
  48. package/dist/comprehension-quiz/question-generator.d.ts +117 -0
  49. package/dist/comprehension-quiz/question-generator.js +203 -0
  50. package/dist/comprehension-quiz/quiz-executor.d.ts +75 -0
  51. package/dist/comprehension-quiz/quiz-executor.js +181 -0
  52. package/dist/comprehension-quiz/types.d.ts +137 -0
  53. package/dist/comprehension-quiz/types.js +11 -0
  54. package/dist/core/archive.d.ts +14 -1
  55. package/dist/core/archive.js +1350 -190
  56. package/dist/core/artifact-graph/graph.d.ts +28 -9
  57. package/dist/core/artifact-graph/graph.js +94 -43
  58. package/dist/core/artifact-graph/index.d.ts +1 -1
  59. package/dist/core/artifact-graph/index.js +1 -1
  60. package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
  61. package/dist/core/artifact-graph/instruction-loader.js +70 -12
  62. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  63. package/dist/core/artifact-graph/outputs.js +67 -5
  64. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  65. package/dist/core/artifact-graph/resolver.js +69 -15
  66. package/dist/core/artifact-graph/types.js +21 -3
  67. package/dist/core/available-tools.d.ts +2 -2
  68. package/dist/core/available-tools.js +14 -3
  69. package/dist/core/change-metadata/schema.d.ts +2 -0
  70. package/dist/core/change-metadata/schema.js +13 -0
  71. package/dist/core/change-status-policy.d.ts +10 -1
  72. package/dist/core/change-status-policy.js +30 -2
  73. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  74. package/dist/core/command-generation/adapters/amazon-q.js +11 -5
  75. package/dist/core/command-generation/adapters/antigravity.js +6 -5
  76. package/dist/core/command-generation/adapters/auggie.js +7 -6
  77. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  78. package/dist/core/command-generation/adapters/bob.js +11 -10
  79. package/dist/core/command-generation/adapters/claude.d.ts +1 -1
  80. package/dist/core/command-generation/adapters/claude.js +12 -17
  81. package/dist/core/command-generation/adapters/cline.js +5 -5
  82. package/dist/core/command-generation/adapters/codebuddy.js +8 -7
  83. package/dist/core/command-generation/adapters/continue.js +8 -7
  84. package/dist/core/command-generation/adapters/costrict.js +7 -6
  85. package/dist/core/command-generation/adapters/crush.js +9 -9
  86. package/dist/core/command-generation/adapters/cursor.js +8 -8
  87. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  88. package/dist/core/command-generation/adapters/devin.js +36 -0
  89. package/dist/core/command-generation/adapters/factory.js +7 -6
  90. package/dist/core/command-generation/adapters/gemini.js +40 -5
  91. package/dist/core/command-generation/adapters/github-copilot.js +6 -5
  92. package/dist/core/command-generation/adapters/iflow.js +9 -8
  93. package/dist/core/command-generation/adapters/index.d.ts +4 -2
  94. package/dist/core/command-generation/adapters/index.js +4 -2
  95. package/dist/core/command-generation/adapters/junie.js +6 -5
  96. package/dist/core/command-generation/adapters/kilocode.js +1 -1
  97. package/dist/core/command-generation/adapters/kiro.js +6 -5
  98. package/dist/core/command-generation/adapters/lingma.js +9 -9
  99. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  100. package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
  101. package/dist/core/command-generation/adapters/opencode.js +6 -8
  102. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  103. package/dist/core/command-generation/adapters/pi.js +7 -10
  104. package/dist/core/command-generation/adapters/qoder.js +9 -9
  105. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  106. package/dist/core/command-generation/adapters/qwen.js +14 -9
  107. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  108. package/dist/core/command-generation/adapters/roocode.js +9 -9
  109. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  110. package/dist/core/command-generation/adapters/trae.js +28 -0
  111. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  112. package/dist/core/command-generation/adapters/zcode.js +33 -0
  113. package/dist/core/command-generation/generator.d.ts +8 -0
  114. package/dist/core/command-generation/generator.js +15 -1
  115. package/dist/core/command-generation/index.d.ts +1 -1
  116. package/dist/core/command-generation/index.js +1 -1
  117. package/dist/core/command-generation/invocation.d.ts +73 -0
  118. package/dist/core/command-generation/invocation.js +77 -0
  119. package/dist/core/command-generation/registry.js +8 -4
  120. package/dist/core/command-generation/types.d.ts +9 -2
  121. package/dist/core/command-generation/yaml.d.ts +20 -6
  122. package/dist/core/command-generation/yaml.js +31 -21
  123. package/dist/core/command-surface.d.ts +15 -0
  124. package/dist/core/command-surface.js +33 -0
  125. package/dist/core/completions/command-registry.js +16 -61
  126. package/dist/core/completions/generators/bash-generator.js +41 -41
  127. package/dist/core/completions/generators/fish-generator.js +7 -7
  128. package/dist/core/completions/generators/powershell-generator.js +44 -37
  129. package/dist/core/completions/generators/zsh-generator.js +33 -33
  130. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  131. package/dist/core/completions/installers/zsh-installer.js +20 -4
  132. package/dist/core/completions/templates/bash-templates.js +24 -24
  133. package/dist/core/completions/templates/fish-templates.js +38 -38
  134. package/dist/core/completions/templates/powershell-templates.js +28 -28
  135. package/dist/core/completions/templates/zsh-templates.js +39 -39
  136. package/dist/core/config-prompts.d.ts +1 -1
  137. package/dist/core/config-prompts.js +14 -1
  138. package/dist/core/config-schema.d.ts +10 -1
  139. package/dist/core/config-schema.js +67 -1
  140. package/dist/core/config.d.ts +15 -0
  141. package/dist/core/config.js +46 -5
  142. package/dist/core/file-state.js +56 -24
  143. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  144. package/dist/core/github-copilot/cloud-agent.js +497 -0
  145. package/dist/core/global-config.d.ts +16 -0
  146. package/dist/core/init.d.ts +58 -0
  147. package/dist/core/init.js +474 -73
  148. package/dist/core/legacy-cleanup.d.ts +59 -0
  149. package/dist/core/legacy-cleanup.js +253 -24
  150. package/dist/core/list.js +23 -16
  151. package/dist/core/migration.d.ts +73 -1
  152. package/dist/core/migration.js +360 -11
  153. package/dist/core/modify/conflict-detector.d.ts +26 -0
  154. package/dist/core/modify/conflict-detector.js +62 -0
  155. package/dist/core/modify/dependency-propagator.d.ts +38 -0
  156. package/dist/core/modify/dependency-propagator.js +103 -0
  157. package/dist/core/modify/history-tracker.d.ts +34 -0
  158. package/dist/core/modify/history-tracker.js +114 -0
  159. package/dist/core/modify/index.d.ts +7 -0
  160. package/dist/core/modify/index.js +7 -0
  161. package/dist/core/onboarding-commands.d.ts +30 -0
  162. package/dist/core/onboarding-commands.js +38 -0
  163. package/dist/core/openspec-root.js +18 -15
  164. package/dist/core/parsers/change-parser.d.ts +17 -2
  165. package/dist/core/parsers/change-parser.js +35 -21
  166. package/dist/core/parsers/code-fence.d.ts +15 -0
  167. package/dist/core/parsers/code-fence.js +49 -0
  168. package/dist/core/parsers/markdown-parser.d.ts +0 -3
  169. package/dist/core/parsers/markdown-parser.js +7 -60
  170. package/dist/core/parsers/requirement-blocks.d.ts +29 -0
  171. package/dist/core/parsers/requirement-blocks.js +150 -31
  172. package/dist/core/parsers/requirement-text.d.ts +48 -0
  173. package/dist/core/parsers/requirement-text.js +98 -0
  174. package/dist/core/parsers/spec-structure.d.ts +1 -1
  175. package/dist/core/parsers/spec-structure.js +20 -30
  176. package/dist/core/profile-sync-drift.d.ts +0 -8
  177. package/dist/core/profile-sync-drift.js +49 -50
  178. package/dist/core/profiles.d.ts +2 -2
  179. package/dist/core/profiles.js +2 -2
  180. package/dist/core/project-config.d.ts +27 -18
  181. package/dist/core/project-config.js +101 -34
  182. package/dist/core/references.js +42 -4
  183. package/dist/core/relationship-health.d.ts +8 -0
  184. package/dist/core/relationship-health.js +13 -0
  185. package/dist/core/root-selection.d.ts +9 -6
  186. package/dist/core/root-selection.js +42 -5
  187. package/dist/core/schemas/base.schema.js +7 -2
  188. package/dist/core/shared/allowed-tools.d.ts +12 -0
  189. package/dist/core/shared/allowed-tools.js +12 -0
  190. package/dist/core/shared/index.d.ts +1 -0
  191. package/dist/core/shared/index.js +1 -0
  192. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  193. package/dist/core/shared/skill-content-equivalence.js +41 -0
  194. package/dist/core/shared/skill-generation.js +17 -13
  195. package/dist/core/shared/skill-paths.d.ts +13 -0
  196. package/dist/core/shared/skill-paths.js +23 -0
  197. package/dist/core/shared/tool-detection.d.ts +27 -7
  198. package/dist/core/shared/tool-detection.js +167 -37
  199. package/dist/core/shared-skill-target.d.ts +16 -0
  200. package/dist/core/shared-skill-target.js +156 -0
  201. package/dist/core/specs-apply.d.ts +80 -33
  202. package/dist/core/specs-apply.js +667 -145
  203. package/dist/core/store/git.d.ts +13 -0
  204. package/dist/core/store/git.js +24 -0
  205. package/dist/core/store/operations.js +20 -0
  206. package/dist/core/templates/skill-templates.d.ts +3 -2
  207. package/dist/core/templates/skill-templates.js +3 -2
  208. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  209. package/dist/core/templates/workflows/apply-change.js +186 -224
  210. package/dist/core/templates/workflows/archive-change.js +387 -260
  211. package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
  212. package/dist/core/templates/workflows/continue-change.js +210 -219
  213. package/dist/core/templates/workflows/explore.js +479 -443
  214. package/dist/core/templates/workflows/feedback.js +97 -97
  215. package/dist/core/templates/workflows/ff-change.js +200 -191
  216. package/dist/core/templates/workflows/modify-change.d.ts +4 -1
  217. package/dist/core/templates/workflows/modify-change.js +193 -71
  218. package/dist/core/templates/workflows/new-change.js +127 -134
  219. package/dist/core/templates/workflows/onboard.js +548 -542
  220. package/dist/core/templates/workflows/propose.js +273 -217
  221. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  222. package/dist/core/templates/workflows/store-selection.js +1 -1
  223. package/dist/core/templates/workflows/sync-specs.js +500 -273
  224. package/dist/core/templates/workflows/update-change.d.ts +10 -0
  225. package/dist/core/templates/workflows/update-change.js +177 -0
  226. package/dist/core/templates/workflows/verify-change.js +324 -326
  227. package/dist/core/update.d.ts +40 -5
  228. package/dist/core/update.js +411 -96
  229. package/dist/core/validation/constants.d.ts +4 -1
  230. package/dist/core/validation/constants.js +4 -1
  231. package/dist/core/validation/plan-validator.d.ts +8 -0
  232. package/dist/core/validation/plan-validator.js +45 -0
  233. package/dist/core/validation/task-numbering.d.ts +16 -0
  234. package/dist/core/validation/task-numbering.js +57 -0
  235. package/dist/core/validation/validator.d.ts +27 -3
  236. package/dist/core/validation/validator.js +337 -48
  237. package/dist/core/version-check.d.ts +152 -0
  238. package/dist/core/version-check.js +731 -0
  239. package/dist/core/view.js +13 -18
  240. package/dist/prompts/searchable-multi-select.js +1 -1
  241. package/dist/telemetry/caller-detection.d.ts +27 -0
  242. package/dist/telemetry/caller-detection.js +85 -0
  243. package/dist/telemetry/caller-detection.test.d.ts +5 -0
  244. package/dist/telemetry/config.d.ts +48 -2
  245. package/dist/telemetry/config.js +113 -11
  246. package/dist/telemetry/config.test.d.ts +5 -0
  247. package/dist/telemetry/deduplication.d.ts +35 -0
  248. package/dist/telemetry/deduplication.js +82 -0
  249. package/dist/telemetry/deduplication.test.d.ts +5 -0
  250. package/dist/telemetry/identity.d.ts +62 -22
  251. package/dist/telemetry/identity.js +147 -102
  252. package/dist/telemetry/index.d.ts +126 -15
  253. package/dist/telemetry/index.js +569 -37
  254. package/dist/telemetry/sanitization.d.ts +40 -0
  255. package/dist/telemetry/sanitization.js +114 -0
  256. package/dist/telemetry/sanitization.test.d.ts +5 -0
  257. package/dist/telemetry/throttling.d.ts +20 -0
  258. package/dist/telemetry/throttling.js +70 -0
  259. package/dist/ui/welcome-screen.d.ts +9 -1
  260. package/dist/ui/welcome-screen.js +91 -38
  261. package/dist/utils/change-metadata.d.ts +43 -0
  262. package/dist/utils/change-metadata.js +123 -7
  263. package/dist/utils/change-utils.d.ts +10 -4
  264. package/dist/utils/change-utils.js +20 -13
  265. package/dist/utils/ci.d.ts +12 -0
  266. package/dist/utils/ci.js +16 -0
  267. package/dist/utils/command-references.d.ts +92 -6
  268. package/dist/utils/command-references.js +180 -7
  269. package/dist/utils/date.d.ts +8 -0
  270. package/dist/utils/date.js +13 -0
  271. package/dist/utils/file-system.d.ts +10 -0
  272. package/dist/utils/file-system.js +70 -0
  273. package/dist/utils/index.d.ts +1 -1
  274. package/dist/utils/index.js +1 -1
  275. package/dist/utils/interactive.d.ts +23 -0
  276. package/dist/utils/interactive.js +32 -0
  277. package/dist/utils/item-discovery.d.ts +19 -0
  278. package/dist/utils/item-discovery.js +30 -48
  279. package/dist/utils/shell-detection.d.ts +1 -1
  280. package/dist/utils/shell-detection.js +64 -11
  281. package/dist/utils/spec-discovery.d.ts +40 -0
  282. package/dist/utils/spec-discovery.js +115 -0
  283. package/dist/utils/task-progress.d.ts +29 -1
  284. package/dist/utils/task-progress.js +109 -15
  285. package/package.json +96 -84
  286. package/schemas/spec-driven/schema.yaml +252 -184
  287. package/schemas/spec-driven/templates/design.md +19 -19
  288. package/schemas/spec-driven/templates/plan-template.md +91 -0
  289. package/schemas/spec-driven/templates/proposal.md +29 -24
  290. package/schemas/spec-driven/templates/spec.md +11 -8
  291. package/schemas/spec-driven/templates/tasks.md +9 -9
  292. package/scripts/postinstall.js +83 -83
  293. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  294. package/dist/core/command-generation/adapters/codex.js +0 -39
  295. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  296. package/dist/core/command-generation/adapters/windsurf.js +0 -38
  297. package/dist/core/comprehension/config.d.ts +0 -20
  298. package/dist/core/comprehension/config.js +0 -23
  299. package/dist/core/comprehension/fingerprint.d.ts +0 -14
  300. package/dist/core/comprehension/fingerprint.js +0 -33
  301. package/dist/core/comprehension/index.d.ts +0 -57
  302. package/dist/core/comprehension/index.js +0 -87
  303. package/dist/core/comprehension/pass-record.d.ts +0 -29
  304. package/dist/core/comprehension/pass-record.js +0 -64
  305. package/dist/core/comprehension/stats.d.ts +0 -35
  306. package/dist/core/comprehension/stats.js +0 -133
  307. package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
  308. package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
  309. package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
  310. package/dist/core/templates/workflows/mcp-guidance.js +0 -131
  311. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
  312. package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
  313. package/dist/telemetry/caller.d.ts +0 -5
  314. package/dist/telemetry/caller.js +0 -29
  315. package/dist/telemetry/client.d.ts +0 -27
  316. package/dist/telemetry/client.js +0 -127
  317. package/dist/telemetry/command-context.d.ts +0 -13
  318. package/dist/telemetry/command-context.js +0 -59
  319. package/dist/telemetry/comprehension.d.ts +0 -44
  320. package/dist/telemetry/comprehension.js +0 -105
  321. package/dist/telemetry/content.d.ts +0 -10
  322. package/dist/telemetry/content.js +0 -56
  323. package/dist/telemetry/git-stats.d.ts +0 -12
  324. package/dist/telemetry/git-stats.js +0 -69
  325. package/dist/telemetry/identify-cache.d.ts +0 -7
  326. package/dist/telemetry/identify-cache.js +0 -47
  327. package/dist/telemetry/input.d.ts +0 -17
  328. package/dist/telemetry/input.js +0 -68
  329. package/dist/telemetry/marker.d.ts +0 -37
  330. package/dist/telemetry/marker.js +0 -67
  331. package/dist/telemetry/workflow.d.ts +0 -85
  332. package/dist/telemetry/workflow.js +0 -318
  333. package/schemas/spec-driven/templates/plan.md +0 -19
@@ -4,9 +4,15 @@ import { SpecSchema, ChangeSchema } from '../schemas/index.js';
4
4
  import { MarkdownParser } from '../parsers/markdown-parser.js';
5
5
  import { ChangeParser } from '../parsers/change-parser.js';
6
6
  import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
7
- import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
7
+ import { parseDeltaSpec, foldRequirementName, normalizeRequirementName, extractRequirementsSection, findMissingCurrentScenarios, } from '../parsers/requirement-blocks.js';
8
+ import { extractRequirementBody as extractRequirementBodyShared, containsShallOrMust as containsShallOrMustShared, countScenarios as countScenariosShared, } from '../parsers/requirement-text.js';
8
9
  import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
9
10
  import { FileSystemUtils } from '../../utils/file-system.js';
11
+ import { discoverSpecFiles, hasAnyFileUnder } from '../../utils/spec-discovery.js';
12
+ import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../../utils/change-metadata.js';
13
+ import { resolveTaskFilesForChange } from '../../utils/task-progress.js';
14
+ import { findTaskNumberingIssues } from './task-numbering.js';
15
+ import { getPackageSchemasDir, getSchemaDir } from '../artifact-graph/index.js';
10
16
  export class Validator {
11
17
  strictMode;
12
18
  constructor(strictMode = false) {
@@ -66,8 +72,20 @@ export class Validator {
66
72
  const parser = new ChangeParser(content, changeDir);
67
73
  const change = await parser.parseChangeWithDeltas(changeName);
68
74
  const result = ChangeSchema.safeParse(change);
75
+ const marker = readSkipSpecsMarker(changeDir);
76
+ if (marker.invalidReason) {
77
+ issues.push({ level: 'ERROR', path: METADATA_FILENAME, message: this.formatInvalidMarkerMessage(marker.invalidReason) });
78
+ }
69
79
  if (!result.success) {
70
- issues.push(...this.convertZodErrors(result.error));
80
+ let zodIssues = this.convertZodErrors(result.error);
81
+ // Only the no-deltas error is marker-aware here: the marker+files
82
+ // conflict is validateChangeDeltaSpecs's job, and every caller of
83
+ // this proposal-level pass (archive's non-blocking warnings) pairs
84
+ // it with that gate.
85
+ if (marker.declared) {
86
+ zodIssues = zodIssues.filter(issue => !issue.message.startsWith(VALIDATION_MESSAGES.CHANGE_NO_DELTAS));
87
+ }
88
+ issues.push(...zodIssues);
71
89
  }
72
90
  issues.push(...this.applyChangeRules(change, content));
73
91
  }
@@ -86,24 +104,47 @@ export class Validator {
86
104
  * Validate delta-formatted spec files under a change directory.
87
105
  * Enforces:
88
106
  * - At least one delta across all files
89
- * - ADDED/MODIFIED: each requirement has SHALL/MUST and at least one scenario
107
+ * - ADDED/MODIFIED: each requirement has at least one scenario; missing
108
+ * English SHALL/MUST keywords are guidance unless strict mode is enabled
90
109
  * - REMOVED: names only; no scenario/description required
91
110
  * - RENAMED: pairs well-formed
92
111
  * - No duplicates within sections; no cross-section conflicts per spec
112
+ *
113
+ * When `options.mainSpecsDir` is given, MODIFIED blocks are also checked
114
+ * against the current main specs for the scenario loss archive refuses to
115
+ * apply (#1477). When `options.projectRoot` is given, the schema's tracked
116
+ * task files are checked for ambiguous numbering (#1520). Omitting either
117
+ * option keeps existing library and archive callers behaving as before.
93
118
  */
94
- async validateChangeDeltaSpecs(changeDir) {
119
+ async validateChangeDeltaSpecs(changeDir, options = {}) {
95
120
  const issues = [];
96
121
  const specsDir = path.join(changeDir, 'specs');
97
122
  let totalDeltas = 0;
123
+ let hasRootLevelSpec = false;
98
124
  const missingHeaderSpecs = [];
99
125
  const emptySectionSpecs = [];
100
126
  try {
101
- const entries = await fs.readdir(specsDir, { withFileTypes: true });
102
- for (const entry of entries) {
103
- if (!entry.isDirectory())
104
- continue;
105
- const specName = entry.name;
106
- const specFile = path.join(specsDir, specName, 'spec.md');
127
+ // Discover delta specs through the same helper the change parser, show,
128
+ // apply, and archive use, so validate never accepts a layout the merge
129
+ // path silently skips (#1385). It finds spec.md at any depth, covering
130
+ // both specs/<capability>/spec.md and the nested multi-area
131
+ // specs/<area>/<capability>/spec.md layout (#1182b).
132
+ const discoveredSpecs = await discoverSpecFiles(specsDir);
133
+ // A spec.md directly at the specs/ root has no capability folder, so the
134
+ // merge path drops it: without this error the change validates clean and
135
+ // archives while its requirements never reach openspec/specs/ (#1385).
136
+ // Only a regular file counts — a *directory* named spec.md is a capability
137
+ // folder like any other, and discoverSpecFiles reads it normally.
138
+ const rootSpecStat = await fs.stat(path.join(specsDir, 'spec.md')).catch(() => null);
139
+ hasRootLevelSpec = rootSpecStat?.isFile() === true;
140
+ if (hasRootLevelSpec) {
141
+ issues.push({
142
+ level: 'ERROR',
143
+ path: 'spec.md',
144
+ message: 'Delta spec found at specs/spec.md. Delta specs must live under a capability path (e.g. specs/<capability-path>/spec.md) — a file at the specs/ root is ignored when the change is applied or archived.',
145
+ });
146
+ }
147
+ for (const { id: specId, specFile } of discoveredSpecs) {
107
148
  let content;
108
149
  try {
109
150
  content = await fs.readFile(specFile, 'utf-8');
@@ -112,7 +153,24 @@ export class Validator {
112
153
  continue;
113
154
  }
114
155
  const plan = parseDeltaSpec(content);
115
- const entryPath = `${specName}/spec.md`;
156
+ const entryPath = FileSystemUtils.toPosixPath(path.relative(specsDir, specFile));
157
+ // Surface (as INFO, never a failure) the non-canonical level-3 headers
158
+ // the delta reader skipped while parsing ADDED/MODIFIED sections —
159
+ // without this note a stray divider like "### Documentation
160
+ // Requirements" would pass validate <change> while failing
161
+ // archive/validate <spec>. The list comes from the parse itself, so it
162
+ // reflects exactly what the reader skipped.
163
+ for (const stray of plan.skippedHeaders) {
164
+ const nameless = /^requirement:?$/i.test(stray.header);
165
+ issues.push({
166
+ level: 'INFO',
167
+ path: entryPath,
168
+ line: stray.line,
169
+ message: nameless
170
+ ? `Header "### ${stray.header}" in ${stray.section} is missing a requirement name and is ignored by validation. Add a name, e.g. "### Requirement: <name>".`
171
+ : `Header "### ${stray.header}" in ${stray.section} is not a "### Requirement:" header and is ignored by validation. Use "### Requirement: ${stray.header}" if it should be validated as a requirement.`,
172
+ });
173
+ }
116
174
  const sectionNames = [];
117
175
  if (plan.sectionPresence.added)
118
176
  sectionNames.push('## ADDED Requirements');
@@ -147,10 +205,20 @@ export class Validator {
147
205
  }
148
206
  const requirementText = this.extractRequirementText(block.raw);
149
207
  if (!requirementText) {
150
- issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" is missing requirement text` });
208
+ issues.push({
209
+ level: 'ERROR',
210
+ path: entryPath,
211
+ message: this.containsShallOrMust(block.name)
212
+ ? this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name)
213
+ : `ADDED "${block.name}" is missing requirement text`,
214
+ });
151
215
  }
152
216
  else if (!this.containsShallOrMust(requirementText)) {
153
- issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage('ADDED', block.name) });
217
+ issues.push({
218
+ level: 'WARNING',
219
+ path: entryPath,
220
+ message: this.buildMissingShallOrMustMessage(`ADDED "${block.name}"`, block.name, true),
221
+ });
154
222
  }
155
223
  const scenarioCount = this.countScenarios(block.raw);
156
224
  if (scenarioCount < 1) {
@@ -169,16 +237,33 @@ export class Validator {
169
237
  }
170
238
  const requirementText = this.extractRequirementText(block.raw);
171
239
  if (!requirementText) {
172
- issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" is missing requirement text` });
240
+ issues.push({
241
+ level: 'ERROR',
242
+ path: entryPath,
243
+ message: this.containsShallOrMust(block.name)
244
+ ? this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name)
245
+ : `MODIFIED "${block.name}" is missing requirement text`,
246
+ });
173
247
  }
174
248
  else if (!this.containsShallOrMust(requirementText)) {
175
- issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage('MODIFIED', block.name) });
249
+ issues.push({
250
+ level: 'WARNING',
251
+ path: entryPath,
252
+ message: this.buildMissingShallOrMustMessage(`MODIFIED "${block.name}"`, block.name, true),
253
+ });
176
254
  }
177
255
  const scenarioCount = this.countScenarios(block.raw);
178
256
  if (scenarioCount < 1) {
179
257
  issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario` });
180
258
  }
181
259
  }
260
+ // Run archive's scenario-loss check here too, so the change fails at
261
+ // authoring time instead of days later at archive time (#1477).
262
+ if (options.mainSpecsDir && plan.modified.length > 0) {
263
+ const mainSpecFile = path.join(options.mainSpecsDir, ...specId.split('/'), 'spec.md');
264
+ FileSystemUtils.assertPathWithin(path.dirname(mainSpecFile), mainSpecFile);
265
+ issues.push(...(await this.findScenarioLossIssues(plan.modified, plan.renamed, mainSpecFile, entryPath, path.dirname(mainSpecFile))));
266
+ }
182
267
  // Validate REMOVED (names only)
183
268
  for (const name of plan.removed) {
184
269
  const key = normalizeRequirementName(name);
@@ -231,11 +316,30 @@ export class Validator {
231
316
  if (addedNames.has(toKey)) {
232
317
  issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
233
318
  }
319
+ // Folded comparison: a case/whitespace variant of the FROM header
320
+ // in REMOVED is the same contradiction, not a different name.
321
+ const removedFoldMatch = [...removedNames].find((r) => foldRequirementName(r) === foldRequirementName(fromKey));
322
+ if (removedFoldMatch !== undefined) {
323
+ issues.push({
324
+ level: 'ERROR',
325
+ path: entryPath,
326
+ message: `Requirement present in both RENAMED and REMOVED: "${from}"` +
327
+ (removedFoldMatch === fromKey ? '' : ` (REMOVED spells it "${removedFoldMatch}")`),
328
+ });
329
+ }
234
330
  }
235
331
  }
236
332
  }
237
- catch {
238
- // If no specs dir, treat as no deltas
333
+ catch (error) {
334
+ // A missing specs dir (or a stray `specs` file) means no deltas;
335
+ // anything else (EACCES, EIO) must stay loud — discoverSpecFiles
336
+ // documents that silently dropping an unreadable capability recreates
337
+ // the data-loss class it prevents, and archive lets the same error
338
+ // propagate.
339
+ const code = error?.code;
340
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
341
+ throw error;
342
+ }
239
343
  }
240
344
  for (const { path: specPath, sections } of emptySectionSpecs) {
241
345
  issues.push({
@@ -251,11 +355,185 @@ export class Validator {
251
355
  message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
252
356
  });
253
357
  }
254
- if (totalDeltas === 0) {
255
- issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
358
+ const marker = readSkipSpecsMarker(changeDir);
359
+ if (marker.invalidReason) {
360
+ issues.push({ level: 'ERROR', path: METADATA_FILENAME, message: this.formatInvalidMarkerMessage(marker.invalidReason) });
361
+ }
362
+ // ANY file under specs/ contradicts the marker - not just parsed deltas.
363
+ // Headerless or stray files would be silently dropped at archive time (and
364
+ // some still satisfy the artifact graph's specs/** glob) while the change
365
+ // claims to have nothing, so they must surface as an explicit conflict.
366
+ // Probed only when the marker is declared, and unreadable specs/ (a stray
367
+ // `specs` file, permission errors) fails closed as a conflict: the marker
368
+ // claims nothing is there, and validate must not crash where the
369
+ // historical path degraded to "no deltas".
370
+ const skipSpecs = marker.declared;
371
+ let specsDirHasFiles = false;
372
+ if (skipSpecs) {
373
+ try {
374
+ specsDirHasFiles = await hasAnyFileUnder(specsDir);
375
+ }
376
+ catch {
377
+ specsDirHasFiles = true;
378
+ }
379
+ }
380
+ if (skipSpecs && specsDirHasFiles) {
381
+ issues.push({ level: 'ERROR', path: 'file', message: VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_CONFLICT });
382
+ }
383
+ // The root-level error already names the file and the fix; adding "No
384
+ // deltas found" on top would contradict it, since the deltas are sitting in
385
+ // the file just reported.
386
+ if (totalDeltas === 0 && !hasRootLevelSpec) {
387
+ if (skipSpecs && !specsDirHasFiles) {
388
+ issues.push({ level: 'INFO', path: 'file', message: VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_ACCEPTED });
389
+ }
390
+ else if (!skipSpecs) {
391
+ issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
392
+ }
393
+ }
394
+ if (options.projectRoot) {
395
+ issues.push(...await this.collectTaskNumberingIssues(changeDir, options.projectRoot));
256
396
  }
257
397
  return this.createReport(issues);
258
398
  }
399
+ async collectTaskNumberingIssues(changeDir, projectRoot) {
400
+ try {
401
+ const schemaName = resolveSchemaForChange(changeDir, undefined, projectRoot).replace(/\.ya?ml$/, '');
402
+ const schemaDir = getSchemaDir(schemaName, projectRoot);
403
+ const builtInSchemaDir = path.join(getPackageSchemasDir(), 'spec-driven');
404
+ if (schemaName !== 'spec-driven' ||
405
+ schemaDir === null ||
406
+ FileSystemUtils.canonicalizeExistingPath(schemaDir) !==
407
+ FileSystemUtils.canonicalizeExistingPath(builtInSchemaDir)) {
408
+ return [];
409
+ }
410
+ }
411
+ catch {
412
+ return [];
413
+ }
414
+ let taskFiles;
415
+ try {
416
+ taskFiles = resolveTaskFilesForChange(changeDir, projectRoot);
417
+ }
418
+ catch {
419
+ return [];
420
+ }
421
+ if (taskFiles.length === 0) {
422
+ taskFiles = [path.join(changeDir, 'tasks.md')];
423
+ }
424
+ const documents = [];
425
+ for (const taskFile of taskFiles) {
426
+ let content;
427
+ try {
428
+ content = await fs.readFile(taskFile, 'utf-8');
429
+ }
430
+ catch {
431
+ continue;
432
+ }
433
+ documents.push({
434
+ path: FileSystemUtils.toPosixPath(path.relative(changeDir, taskFile)),
435
+ content,
436
+ });
437
+ }
438
+ documents.sort((left, right) => left.path.localeCompare(right.path));
439
+ return findTaskNumberingIssues(documents).map((issue) => ({
440
+ level: 'WARNING',
441
+ path: issue.path,
442
+ line: issue.line,
443
+ message: issue.message,
444
+ }));
445
+ }
446
+ /**
447
+ * Report MODIFIED requirements whose block omits a scenario the main spec
448
+ * still carries. Uses the same comparison archive applies, so validate can
449
+ * only report what archive would refuse.
450
+ *
451
+ * Silent when the main spec or the requirement header is absent: applying a
452
+ * MODIFIED against a base that is not there yet is a different failure (a
453
+ * sister change still in flight is the legitimate case), and archive is the
454
+ * gate for it. A spec that exists but cannot be read is not absent, though —
455
+ * archive aborts on it, so reporting it beats calling the change valid.
456
+ */
457
+ async findScenarioLossIssues(modified, renamed, mainSpecFile, entryPath, mainSpecRoot) {
458
+ let mainContent;
459
+ FileSystemUtils.assertPathWithin(mainSpecRoot, mainSpecFile);
460
+ try {
461
+ mainContent = await fs.readFile(mainSpecFile, 'utf-8');
462
+ }
463
+ catch (error) {
464
+ const code = error?.code;
465
+ // Reported only for the codes that mean the file itself is unusable, and
466
+ // will be just as unusable when archive reads it. Everything else -
467
+ // ENOENT/ENOTDIR ("no main spec"), and transient resource errors like
468
+ // EMFILE that say nothing about the file - stays silent rather than
469
+ // failing a change that is fine. `validate --all` reads six changes at
470
+ // once, so a resource error must never become a verdict.
471
+ const UNUSABLE = new Set(['EACCES', 'EPERM', 'EISDIR', 'ELOOP', 'ENAMETOOLONG']);
472
+ if (!code || !UNUSABLE.has(code))
473
+ return [];
474
+ return [
475
+ {
476
+ level: 'ERROR',
477
+ path: entryPath,
478
+ message: `Could not read ${FileSystemUtils.toPosixPath(mainSpecFile)} to check the MODIFIED requirements against it ` +
479
+ `(${code}). Archive reads the same file, so fix the file before archiving.`,
480
+ },
481
+ ];
482
+ }
483
+ const currentBlocks = new Map();
484
+ for (const block of extractRequirementsSection(mainContent).bodyBlocks) {
485
+ currentBlocks.set(normalizeRequirementName(block.name), block);
486
+ }
487
+ // Archive applies RENAMED before MODIFIED, so a MODIFIED naming the new
488
+ // header is compared against the renamed block's scenarios. Fall back to
489
+ // the old header, or a rename-plus-modify pair would skip the check.
490
+ const renamedFrom = new Map(renamed.map(({ from, to }) => [normalizeRequirementName(to), normalizeRequirementName(from)]));
491
+ // Walked, not looked up once: renames chain (A→B then B→C leaves C holding
492
+ // A's block), and the visited set stops a cycle from looping forever. Every
493
+ // name in a rename cycle is also a rename FROM, so the skip above already
494
+ // keeps the walk out of one; the guard stays because the cost of being
495
+ // wrong about that is a hung CLI, not a wrong message.
496
+ const currentBlockFor = (name) => {
497
+ const visited = new Set();
498
+ let key = name;
499
+ while (key !== undefined && !visited.has(key)) {
500
+ const block = currentBlocks.get(key);
501
+ if (block)
502
+ return block;
503
+ visited.add(key);
504
+ key = renamedFrom.get(key);
505
+ }
506
+ return undefined;
507
+ };
508
+ // A MODIFIED naming a header the same delta renames away is already
509
+ // reported ("MODIFIED references old name from RENAMED"), and the block it
510
+ // would land on is not the one it names — so any scenario named here would
511
+ // send the author after the wrong requirement.
512
+ const renamedAway = new Set(renamed.map(({ from }) => normalizeRequirementName(from)));
513
+ const issues = [];
514
+ for (const block of modified) {
515
+ const key = normalizeRequirementName(block.name);
516
+ if (renamedAway.has(key))
517
+ continue;
518
+ const current = currentBlockFor(key);
519
+ if (!current)
520
+ continue;
521
+ const missing = findMissingCurrentScenarios(current, block);
522
+ if (missing.length === 0)
523
+ continue;
524
+ issues.push({
525
+ level: 'ERROR',
526
+ path: entryPath,
527
+ message: `MODIFIED "${block.name}" omits scenario(s) the current spec still has: ` +
528
+ `${missing.map(name => `"${name}"`).join(', ')}. ` +
529
+ 'Copy them into the MODIFIED block (a MODIFIED requirement replaces the whole block, so archive refuses to drop them).',
530
+ });
531
+ }
532
+ return issues;
533
+ }
534
+ formatInvalidMarkerMessage(invalidReason) {
535
+ return `${VALIDATION_MESSAGES.CHANGE_SKIP_SPECS_INVALID_METADATA} (${invalidReason})`;
536
+ }
259
537
  convertZodErrors(error) {
260
538
  return error.issues.map(err => {
261
539
  let message = err.message;
@@ -302,6 +580,29 @@ export class Validator {
302
580
  });
303
581
  }
304
582
  });
583
+ // SHALL/MUST body-keyword guidance for main specs (#1156, #243). The main-spec
584
+ // parser collapses the requirement header into `text`, so we recover the
585
+ // header+body pairs here (the same source the delta path trusts) and reuse
586
+ // the delta detection. A non-empty body that omits the English keyword gets
587
+ // guidance, while a missing body remains an error. Emitted exactly once per
588
+ // requirement (the Zod refine that used to emit a generic error is removed).
589
+ extractRequirementsSection(content).bodyBlocks.forEach((block, index) => {
590
+ const requirementText = this.extractRequirementText(block.raw);
591
+ if (!requirementText) {
592
+ issues.push({
593
+ level: 'ERROR',
594
+ path: `requirements[${index}]`,
595
+ message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name),
596
+ });
597
+ }
598
+ else if (!this.containsShallOrMust(requirementText)) {
599
+ issues.push({
600
+ level: 'WARNING',
601
+ path: `requirements[${index}]`,
602
+ message: this.buildMissingShallOrMustMessage(`Requirement "${block.name}"`, block.name, true),
603
+ });
604
+ }
605
+ });
305
606
  return issues;
306
607
  }
307
608
  applyChangeRules(change, content) {
@@ -376,33 +677,19 @@ export class Validator {
376
677
  return report.valid;
377
678
  }
378
679
  extractRequirementText(blockRaw) {
379
- const lines = blockRaw.split('\n');
380
- // Skip header line (index 0)
381
- let i = 1;
382
- // Find the first substantial text line, skipping metadata and blank lines
383
- for (; i < lines.length; i++) {
384
- const line = lines[i];
385
- // Stop at scenario headers
386
- if (/^####\s+/.test(line))
387
- break;
388
- const trimmed = line.trim();
389
- // Skip blank lines
390
- if (trimmed.length === 0)
391
- continue;
392
- // Skip metadata lines (lines starting with ** like **ID**, **Priority**, etc.)
393
- if (/^\*\*[^*]+\*\*:/.test(trimmed))
394
- continue;
395
- // Found first non-metadata, non-blank line - this is the requirement text
396
- return trimmed;
397
- }
398
- // No requirement text found
399
- return undefined;
680
+ // Delegate to the shared, fence-/metadata-/multi-line-aware body reader.
681
+ // Validation intentionally does not use the parser/display header-title
682
+ // fallback for canonical `### Requirement:` blocks: #1280 requires a
683
+ // SHALL/MUST that appears only in the header to receive the body-keyword
684
+ // hint. Line 0 is the `### Requirement: ...` header.
685
+ const [, ...bodyLines] = blockRaw.split('\n');
686
+ return extractRequirementBodyShared(bodyLines) || undefined;
400
687
  }
401
688
  containsShallOrMust(text) {
402
- return /\b(SHALL|MUST)\b/.test(text);
689
+ return containsShallOrMustShared(text);
403
690
  }
404
691
  /**
405
- * Build an error message for a requirement block whose body lacks SHALL/MUST.
692
+ * Build a message for a requirement block whose body lacks SHALL/MUST.
406
693
  *
407
694
  * When the SHALL/MUST keyword already appears in the requirement header (e.g.
408
695
  * `### Requirement: The system SHALL ...`) the original generic error
@@ -411,16 +698,18 @@ export class Validator {
411
698
  * on the requirement body line (the line right after the header), so we point
412
699
  * the author at that exact fix when the keyword is found in the header only.
413
700
  */
414
- buildMissingShallOrMustMessage(action, blockName) {
415
- const base = `${action} "${blockName}" must contain SHALL or MUST`;
701
+ buildMissingShallOrMustMessage(prefix, blockName, guidanceOnly = false) {
702
+ const base = `${prefix} ${guidanceOnly ? 'should' : 'must'} contain SHALL or MUST`;
703
+ const suffix = guidanceOnly ? ' (RFC 2119 best practice for English specs)' : '';
416
704
  if (this.containsShallOrMust(blockName)) {
417
- return `${base} in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.`;
705
+ return `${base} in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.${suffix}`;
418
706
  }
419
- return base;
707
+ return `${base}${suffix}`;
420
708
  }
421
709
  countScenarios(blockRaw) {
422
- const matches = blockRaw.match(/^####\s+/gm);
423
- return matches ? matches.length : 0;
710
+ // Fence-aware count via the shared reader: a `#### Scenario:` inside a fenced
711
+ // example is not a real scenario. Drop the header line (index 0).
712
+ return countScenariosShared(blockRaw.split('\n').slice(1));
424
713
  }
425
714
  formatSectionList(sections) {
426
715
  if (sections.length === 0)
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The registry to ask: only the environment variable npm exports (under
3
+ * `npm run`, or an explicit export). Deliberately not a `registry=` line from
4
+ * any .npmrc — letting file contents choose the destination of an outbound
5
+ * request is a flow worth avoiding for a convenience this small, and a project
6
+ * file would travel with a cloned repository. Anyone on a private mirror can
7
+ * export `npm_config_registry`, or turn the check off entirely.
8
+ */
9
+ export declare function registryUrl(): string;
10
+ /**
11
+ * Compares two semver-ish versions. Returns 1 when a > b, -1 when a < b, 0
12
+ * otherwise. Prereleases sort below their release (1.7.0-beta.1 < 1.7.0).
13
+ */
14
+ export declare function compareVersions(a: string, b: string): number;
15
+ /**
16
+ * Returns the published version when the installed CLI is behind it, otherwise
17
+ * null. Never throws and never blocks for longer than the request timeout.
18
+ */
19
+ export declare function getAvailableCliUpdate(): Promise<string | null>;
20
+ /**
21
+ * Directory the running CLI was loaded from, or null when it cannot be
22
+ * resolved. Shown in the upgrade hint so anyone who upgraded but still runs an
23
+ * old binary — a stale pnpm/volta/npx shim, or two installs on PATH — can see
24
+ * which copy is actually answering.
25
+ */
26
+ export declare function getInstallDir(): string | null;
27
+ /**
28
+ * True when the running CLI resolves from a `node_modules` belonging to the
29
+ * project being updated or any ancestor of it — the hoisted-root layout npm and
30
+ * pnpm workspaces produce. Anchored on the target path rather than the working
31
+ * directory, since `openspec update <path>` and running from a sub-package are
32
+ * both normal. Never throws: process.cwd() fails when the directory has been
33
+ * deleted, and a wrong upgrade hint must not take down a successful update.
34
+ */
35
+ export declare function isProjectLocalInstall(installDir: string | null, projectPath?: string): boolean;
36
+ /**
37
+ * True for the throwaway caches npx/pnpm dlx/bunx unpack into. Telling those
38
+ * users to install globally would create the second copy on PATH they were
39
+ * deliberately avoiding.
40
+ */
41
+ export declare function isEphemeralRunnerInstall(installDir: string | null): boolean;
42
+ /**
43
+ * Directories npm installs global packages into. Derived from the running node
44
+ * rather than by shelling out to `npm prefix -g`, which would cost more than
45
+ * the version check itself. Only a hint: `process.execPath` is realpath'd, so
46
+ * on Homebrew it lands in the Cellar rather than the brew prefix — which is
47
+ * why the install's own layout is the primary signal below.
48
+ */
49
+ export declare function npmGlobalRoots(): string[];
50
+ /**
51
+ * The prefix of an npm global install, read from the install's own shape:
52
+ * `<prefix>/lib/node_modules/<pkg>` on POSIX, `<prefix>/node_modules/<pkg>` on
53
+ * Windows. Self-describing, so it holds for Homebrew, nvm, Debian and anywhere
54
+ * else npm's prefix is not derivable from the node binary. Null when the
55
+ * layout does not match.
56
+ */
57
+ export declare function npmPrefixFromInstallDir(installDir: string | null): string | null;
58
+ /**
59
+ * True only when npm itself owns this copy. Everything else — a pnpm, bun,
60
+ * yarn or volta global — would be made worse by `npm install -g`, which adds a
61
+ * second copy that may not even be the one on PATH.
62
+ */
63
+ export declare function isNpmGlobalInstall(installDir: string | null, roots?: string[]): boolean;
64
+ /**
65
+ * True when the CLI is running from a clone rather than an install. Upgrade
66
+ * advice is meaningless there: the version is whatever the branch says.
67
+ */
68
+ export declare function isSourceCheckout(installDir: string | null): boolean;
69
+ export type PackageManager = 'npm' | 'pnpm' | 'bun' | 'yarn' | 'volta';
70
+ /**
71
+ * The package manager that owns this copy, so the printed command is one the
72
+ * user's setup will actually honor.
73
+ */
74
+ export declare function detectPackageManager(installDir: string | null): PackageManager;
75
+ /**
76
+ * Builds the hint, with the upgrade command chosen for how this copy of the CLI
77
+ * was installed. Pure so every branch is assertable.
78
+ */
79
+ export declare function buildCliUpdateLines(latestVersion: string, installDir: string | null, projectPath: string, options?: {
80
+ withCommand?: boolean;
81
+ }): string[];
82
+ /**
83
+ * The upgrade command for however this copy was installed, plus the reminder
84
+ * that instruction files come from the CLI and so need a second pass.
85
+ */
86
+ export declare function buildUpgradeCommandLines(installDir: string | null, projectPath: string): string[];
87
+ /**
88
+ * Whether we can run the upgrade for the user instead of only printing it.
89
+ *
90
+ * Only an npm-owned global install qualifies, because `npm install -g` is the
91
+ * only command we run: a pnpm/bun/yarn/volta global would get a second copy
92
+ * that may not be the one on PATH, a project dependency belongs to that
93
+ * project's package manager, an npx/dlx cache has nothing to upgrade, and a
94
+ * source checkout is not an install at all.
95
+ */
96
+ export declare function canSelfUpgrade(installDir: string | null, projectPath: string): boolean;
97
+ /**
98
+ * Whether to offer the upgrade rather than just print the command. Kept here,
99
+ * as a pure function of the environment, because the interesting mistakes live
100
+ * in this decision: offering where `npm install -g` cannot help, or asking a
101
+ * question no one can answer.
102
+ */
103
+ export declare function shouldOfferUpgrade(params: {
104
+ installDir: string | null;
105
+ projectPath: string;
106
+ interactive: boolean;
107
+ stdoutIsTty: boolean;
108
+ }): boolean;
109
+ /**
110
+ * The `openspec` npm installs alongside its global package, so the upgrade can
111
+ * be handed to the copy npm just wrote rather than to whatever PATH resolves.
112
+ * Null when it cannot be found, in which case PATH is the only option left.
113
+ */
114
+ export declare function upgradedBinPath(roots?: string[], installDir?: string | null): string | null;
115
+ /**
116
+ * Asks a CLI binary its version. Used to confirm an upgrade actually landed:
117
+ * `npm install -g` exits 0 even when it installed nothing, so its exit code
118
+ * alone cannot justify telling the user they are on a new version.
119
+ */
120
+ export declare function readCliVersion(binPath: string): Promise<string | null>;
121
+ export type UpgradeOutcome = 'upgraded' | 'declined' | 'failed' | 'cancelled' | 'not-on-path';
122
+ /**
123
+ * Offers to run the upgrade and reports what actually happened. The version is
124
+ * read back from the installed binary rather than assumed, so "upgraded" is a
125
+ * fact and a PATH that still answers with the old copy is caught here instead
126
+ * of silently doing nothing.
127
+ */
128
+ export declare function offerCliUpgrade(latestVersion: string): Promise<UpgradeOutcome>;
129
+ /**
130
+ * Runs `openspec update` again with the CLI that was just installed — this
131
+ * process is still the old code, so it cannot write the new workflows itself.
132
+ * Resolves the exit code to pass along; when no `openspec` is on PATH the
133
+ * upgrade still landed but nothing was regenerated, so it says so and
134
+ * resolves 0 rather than reporting a failure the upgrade did not have.
135
+ */
136
+ export declare function rerunUpdateWithUpgradedCli(projectPath: string, options?: {
137
+ force?: boolean;
138
+ binPath?: string;
139
+ }): Promise<number>;
140
+ /**
141
+ * Prints the upgrade hint. Instruction files are generated by the installed
142
+ * CLI, so "up to date" only ever means "matches this CLI" — without this note
143
+ * a stale install looks like a successful update.
144
+ */
145
+ export declare function displayCliUpdateNote(latestVersion: string, projectPath?: string, options?: {
146
+ withCommand?: boolean;
147
+ }): void;
148
+ /**
149
+ * Prints just the manual command, for when the offer was declined or failed.
150
+ */
151
+ export declare function displayUpgradeCommand(projectPath?: string): void;
152
+ //# sourceMappingURL=version-check.d.ts.map