@codewalla_india/openspec 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +272 -191
  3. package/bin/openspec.js +5 -5
  4. package/dist/cli/index.js +99 -62
  5. package/dist/commands/change.d.ts +0 -2
  6. package/dist/commands/change.js +107 -86
  7. package/dist/commands/config.js +14 -16
  8. package/dist/commands/doctor.js +9 -4
  9. package/dist/commands/feedback.js +79 -30
  10. package/dist/commands/modify.d.ts +26 -0
  11. package/dist/commands/modify.js +147 -0
  12. package/dist/commands/schema.js +170 -91
  13. package/dist/commands/show.js +5 -2
  14. package/dist/commands/spec.js +49 -29
  15. package/dist/commands/store.js +15 -21
  16. package/dist/commands/validate.d.ts +8 -0
  17. package/dist/commands/validate.js +43 -20
  18. package/dist/commands/workflow/index.d.ts +2 -2
  19. package/dist/commands/workflow/index.js +1 -1
  20. package/dist/commands/workflow/instructions.d.ts +6 -26
  21. package/dist/commands/workflow/instructions.js +183 -376
  22. package/dist/commands/workflow/new-change.d.ts +0 -4
  23. package/dist/commands/workflow/new-change.js +10 -28
  24. package/dist/commands/workflow/shared.d.ts +34 -19
  25. package/dist/commands/workflow/shared.js +34 -4
  26. package/dist/commands/workflow/status.js +10 -32
  27. package/dist/commands/workflow/templates.js +15 -5
  28. package/dist/commands/workset.d.ts +2 -2
  29. package/dist/commands/workset.js +19 -22
  30. package/dist/comprehension-quiz/fingerprint.d.ts +39 -0
  31. package/dist/comprehension-quiz/fingerprint.js +84 -0
  32. package/dist/comprehension-quiz/index.d.ts +42 -0
  33. package/dist/comprehension-quiz/index.js +47 -0
  34. package/dist/comprehension-quiz/pass-record.d.ts +44 -0
  35. package/dist/comprehension-quiz/pass-record.js +98 -0
  36. package/dist/comprehension-quiz/providers/anthropic.d.ts +34 -0
  37. package/dist/comprehension-quiz/providers/anthropic.js +53 -0
  38. package/dist/comprehension-quiz/providers/factory.d.ts +44 -0
  39. package/dist/comprehension-quiz/providers/factory.js +174 -0
  40. package/dist/comprehension-quiz/providers/interface.d.ts +59 -0
  41. package/dist/comprehension-quiz/providers/interface.js +63 -0
  42. package/dist/comprehension-quiz/providers/local.d.ts +34 -0
  43. package/dist/comprehension-quiz/providers/local.js +53 -0
  44. package/dist/comprehension-quiz/providers/ollama.d.ts +34 -0
  45. package/dist/comprehension-quiz/providers/ollama.js +53 -0
  46. package/dist/comprehension-quiz/providers/openai.d.ts +34 -0
  47. package/dist/comprehension-quiz/providers/openai.js +53 -0
  48. package/dist/comprehension-quiz/question-generator.d.ts +69 -0
  49. package/dist/comprehension-quiz/question-generator.js +141 -0
  50. package/dist/comprehension-quiz/quiz-executor.d.ts +70 -0
  51. package/dist/comprehension-quiz/quiz-executor.js +165 -0
  52. package/dist/comprehension-quiz/types.d.ts +115 -0
  53. package/dist/comprehension-quiz/types.js +11 -0
  54. package/dist/core/archive.d.ts +14 -1
  55. package/dist/core/archive.js +1350 -190
  56. package/dist/core/artifact-graph/graph.d.ts +28 -9
  57. package/dist/core/artifact-graph/graph.js +94 -43
  58. package/dist/core/artifact-graph/index.d.ts +1 -1
  59. package/dist/core/artifact-graph/index.js +1 -1
  60. package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
  61. package/dist/core/artifact-graph/instruction-loader.js +70 -12
  62. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  63. package/dist/core/artifact-graph/outputs.js +67 -5
  64. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  65. package/dist/core/artifact-graph/resolver.js +69 -15
  66. package/dist/core/artifact-graph/types.js +21 -3
  67. package/dist/core/available-tools.d.ts +2 -2
  68. package/dist/core/available-tools.js +14 -3
  69. package/dist/core/change-metadata/schema.d.ts +2 -0
  70. package/dist/core/change-metadata/schema.js +13 -0
  71. package/dist/core/change-status-policy.d.ts +10 -1
  72. package/dist/core/change-status-policy.js +30 -2
  73. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  74. package/dist/core/command-generation/adapters/amazon-q.js +11 -5
  75. package/dist/core/command-generation/adapters/antigravity.js +6 -5
  76. package/dist/core/command-generation/adapters/auggie.js +7 -6
  77. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  78. package/dist/core/command-generation/adapters/bob.js +11 -10
  79. package/dist/core/command-generation/adapters/claude.d.ts +1 -1
  80. package/dist/core/command-generation/adapters/claude.js +12 -17
  81. package/dist/core/command-generation/adapters/cline.js +5 -5
  82. package/dist/core/command-generation/adapters/codebuddy.js +8 -7
  83. package/dist/core/command-generation/adapters/continue.js +8 -7
  84. package/dist/core/command-generation/adapters/costrict.js +7 -6
  85. package/dist/core/command-generation/adapters/crush.js +9 -9
  86. package/dist/core/command-generation/adapters/cursor.js +8 -8
  87. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  88. package/dist/core/command-generation/adapters/devin.js +36 -0
  89. package/dist/core/command-generation/adapters/factory.js +7 -6
  90. package/dist/core/command-generation/adapters/gemini.js +40 -5
  91. package/dist/core/command-generation/adapters/github-copilot.js +6 -5
  92. package/dist/core/command-generation/adapters/iflow.js +9 -8
  93. package/dist/core/command-generation/adapters/index.d.ts +4 -2
  94. package/dist/core/command-generation/adapters/index.js +4 -2
  95. package/dist/core/command-generation/adapters/junie.js +6 -5
  96. package/dist/core/command-generation/adapters/kilocode.js +1 -1
  97. package/dist/core/command-generation/adapters/kiro.js +6 -5
  98. package/dist/core/command-generation/adapters/lingma.js +9 -9
  99. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  100. package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
  101. package/dist/core/command-generation/adapters/opencode.js +6 -8
  102. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  103. package/dist/core/command-generation/adapters/pi.js +7 -10
  104. package/dist/core/command-generation/adapters/qoder.js +9 -9
  105. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  106. package/dist/core/command-generation/adapters/qwen.js +14 -9
  107. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  108. package/dist/core/command-generation/adapters/roocode.js +9 -9
  109. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  110. package/dist/core/command-generation/adapters/trae.js +28 -0
  111. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  112. package/dist/core/command-generation/adapters/zcode.js +33 -0
  113. package/dist/core/command-generation/generator.d.ts +8 -0
  114. package/dist/core/command-generation/generator.js +15 -1
  115. package/dist/core/command-generation/index.d.ts +1 -1
  116. package/dist/core/command-generation/index.js +1 -1
  117. package/dist/core/command-generation/invocation.d.ts +73 -0
  118. package/dist/core/command-generation/invocation.js +77 -0
  119. package/dist/core/command-generation/registry.js +8 -4
  120. package/dist/core/command-generation/types.d.ts +9 -2
  121. package/dist/core/command-generation/yaml.d.ts +20 -6
  122. package/dist/core/command-generation/yaml.js +31 -21
  123. package/dist/core/command-surface.d.ts +15 -0
  124. package/dist/core/command-surface.js +33 -0
  125. package/dist/core/completions/command-registry.js +16 -61
  126. package/dist/core/completions/generators/bash-generator.js +41 -41
  127. package/dist/core/completions/generators/fish-generator.js +7 -7
  128. package/dist/core/completions/generators/powershell-generator.js +44 -37
  129. package/dist/core/completions/generators/zsh-generator.js +33 -33
  130. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  131. package/dist/core/completions/installers/zsh-installer.js +20 -4
  132. package/dist/core/completions/templates/bash-templates.js +24 -24
  133. package/dist/core/completions/templates/fish-templates.js +38 -38
  134. package/dist/core/completions/templates/powershell-templates.js +28 -28
  135. package/dist/core/completions/templates/zsh-templates.js +39 -39
  136. package/dist/core/config-prompts.d.ts +1 -1
  137. package/dist/core/config-prompts.js +14 -1
  138. package/dist/core/config-schema.d.ts +10 -1
  139. package/dist/core/config-schema.js +67 -1
  140. package/dist/core/config.d.ts +15 -0
  141. package/dist/core/config.js +46 -5
  142. package/dist/core/file-state.js +56 -24
  143. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  144. package/dist/core/github-copilot/cloud-agent.js +497 -0
  145. package/dist/core/global-config.d.ts +16 -0
  146. package/dist/core/init.d.ts +58 -0
  147. package/dist/core/init.js +474 -73
  148. package/dist/core/legacy-cleanup.d.ts +59 -0
  149. package/dist/core/legacy-cleanup.js +253 -24
  150. package/dist/core/list.js +23 -16
  151. package/dist/core/migration.d.ts +73 -1
  152. package/dist/core/migration.js +360 -11
  153. package/dist/core/modify/conflict-detector.d.ts +26 -0
  154. package/dist/core/modify/conflict-detector.js +62 -0
  155. package/dist/core/modify/dependency-propagator.d.ts +38 -0
  156. package/dist/core/modify/dependency-propagator.js +103 -0
  157. package/dist/core/modify/history-tracker.d.ts +34 -0
  158. package/dist/core/modify/history-tracker.js +114 -0
  159. package/dist/core/modify/index.d.ts +7 -0
  160. package/dist/core/modify/index.js +7 -0
  161. package/dist/core/onboarding-commands.d.ts +30 -0
  162. package/dist/core/onboarding-commands.js +38 -0
  163. package/dist/core/openspec-root.js +18 -15
  164. package/dist/core/parsers/change-parser.d.ts +17 -2
  165. package/dist/core/parsers/change-parser.js +35 -21
  166. package/dist/core/parsers/code-fence.d.ts +15 -0
  167. package/dist/core/parsers/code-fence.js +49 -0
  168. package/dist/core/parsers/markdown-parser.d.ts +0 -3
  169. package/dist/core/parsers/markdown-parser.js +7 -60
  170. package/dist/core/parsers/requirement-blocks.d.ts +29 -0
  171. package/dist/core/parsers/requirement-blocks.js +150 -31
  172. package/dist/core/parsers/requirement-text.d.ts +48 -0
  173. package/dist/core/parsers/requirement-text.js +98 -0
  174. package/dist/core/parsers/spec-structure.d.ts +1 -1
  175. package/dist/core/parsers/spec-structure.js +20 -30
  176. package/dist/core/profile-sync-drift.d.ts +0 -8
  177. package/dist/core/profile-sync-drift.js +49 -50
  178. package/dist/core/profiles.d.ts +2 -2
  179. package/dist/core/profiles.js +2 -2
  180. package/dist/core/project-config.d.ts +27 -18
  181. package/dist/core/project-config.js +101 -34
  182. package/dist/core/references.js +42 -4
  183. package/dist/core/relationship-health.d.ts +8 -0
  184. package/dist/core/relationship-health.js +13 -0
  185. package/dist/core/root-selection.d.ts +9 -6
  186. package/dist/core/root-selection.js +42 -5
  187. package/dist/core/schemas/base.schema.js +7 -2
  188. package/dist/core/shared/allowed-tools.d.ts +12 -0
  189. package/dist/core/shared/allowed-tools.js +12 -0
  190. package/dist/core/shared/index.d.ts +1 -0
  191. package/dist/core/shared/index.js +1 -0
  192. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  193. package/dist/core/shared/skill-content-equivalence.js +41 -0
  194. package/dist/core/shared/skill-generation.js +17 -13
  195. package/dist/core/shared/skill-paths.d.ts +13 -0
  196. package/dist/core/shared/skill-paths.js +23 -0
  197. package/dist/core/shared/tool-detection.d.ts +27 -7
  198. package/dist/core/shared/tool-detection.js +167 -37
  199. package/dist/core/shared-skill-target.d.ts +16 -0
  200. package/dist/core/shared-skill-target.js +156 -0
  201. package/dist/core/specs-apply.d.ts +80 -33
  202. package/dist/core/specs-apply.js +667 -145
  203. package/dist/core/store/git.d.ts +13 -0
  204. package/dist/core/store/git.js +24 -0
  205. package/dist/core/store/operations.js +20 -0
  206. package/dist/core/templates/skill-templates.d.ts +3 -2
  207. package/dist/core/templates/skill-templates.js +3 -2
  208. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  209. package/dist/core/templates/workflows/apply-change.js +186 -224
  210. package/dist/core/templates/workflows/archive-change.js +387 -260
  211. package/dist/core/templates/workflows/bulk-archive-change.js +655 -475
  212. package/dist/core/templates/workflows/continue-change.js +210 -219
  213. package/dist/core/templates/workflows/explore.js +479 -443
  214. package/dist/core/templates/workflows/feedback.js +97 -97
  215. package/dist/core/templates/workflows/ff-change.js +200 -191
  216. package/dist/core/templates/workflows/modify-change.d.ts +4 -1
  217. package/dist/core/templates/workflows/modify-change.js +237 -115
  218. package/dist/core/templates/workflows/new-change.js +127 -134
  219. package/dist/core/templates/workflows/onboard.js +548 -542
  220. package/dist/core/templates/workflows/propose.js +273 -217
  221. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  222. package/dist/core/templates/workflows/store-selection.js +1 -1
  223. package/dist/core/templates/workflows/sync-specs.js +500 -273
  224. package/dist/core/templates/workflows/update-change.d.ts +10 -0
  225. package/dist/core/templates/workflows/update-change.js +177 -0
  226. package/dist/core/templates/workflows/verify-change.js +324 -326
  227. package/dist/core/update.d.ts +40 -5
  228. package/dist/core/update.js +411 -96
  229. package/dist/core/validation/constants.d.ts +4 -1
  230. package/dist/core/validation/constants.js +4 -1
  231. package/dist/core/validation/plan-validator.d.ts +8 -0
  232. package/dist/core/validation/plan-validator.js +45 -0
  233. package/dist/core/validation/task-numbering.d.ts +16 -0
  234. package/dist/core/validation/task-numbering.js +57 -0
  235. package/dist/core/validation/validator.d.ts +27 -3
  236. package/dist/core/validation/validator.js +337 -48
  237. package/dist/core/version-check.d.ts +152 -0
  238. package/dist/core/version-check.js +731 -0
  239. package/dist/core/view.js +13 -18
  240. package/dist/prompts/searchable-multi-select.js +1 -1
  241. package/dist/telemetry/caller-detection.d.ts +27 -0
  242. package/dist/telemetry/caller-detection.js +85 -0
  243. package/dist/telemetry/caller-detection.test.d.ts +5 -0
  244. package/dist/telemetry/config.d.ts +48 -2
  245. package/dist/telemetry/config.js +113 -11
  246. package/dist/telemetry/config.test.d.ts +5 -0
  247. package/dist/telemetry/deduplication.d.ts +35 -0
  248. package/dist/telemetry/deduplication.js +82 -0
  249. package/dist/telemetry/deduplication.test.d.ts +5 -0
  250. package/dist/telemetry/identity.d.ts +62 -22
  251. package/dist/telemetry/identity.js +147 -102
  252. package/dist/telemetry/index.d.ts +126 -15
  253. package/dist/telemetry/index.js +569 -37
  254. package/dist/telemetry/sanitization.d.ts +40 -0
  255. package/dist/telemetry/sanitization.js +114 -0
  256. package/dist/telemetry/sanitization.test.d.ts +5 -0
  257. package/dist/telemetry/throttling.d.ts +20 -0
  258. package/dist/telemetry/throttling.js +70 -0
  259. package/dist/ui/welcome-screen.d.ts +9 -1
  260. package/dist/ui/welcome-screen.js +91 -38
  261. package/dist/utils/change-metadata.d.ts +43 -0
  262. package/dist/utils/change-metadata.js +123 -7
  263. package/dist/utils/change-utils.d.ts +10 -4
  264. package/dist/utils/change-utils.js +20 -13
  265. package/dist/utils/ci.d.ts +12 -0
  266. package/dist/utils/ci.js +16 -0
  267. package/dist/utils/command-references.d.ts +92 -6
  268. package/dist/utils/command-references.js +180 -7
  269. package/dist/utils/date.d.ts +8 -0
  270. package/dist/utils/date.js +13 -0
  271. package/dist/utils/file-system.d.ts +10 -0
  272. package/dist/utils/file-system.js +70 -0
  273. package/dist/utils/index.d.ts +1 -1
  274. package/dist/utils/index.js +1 -1
  275. package/dist/utils/interactive.d.ts +23 -0
  276. package/dist/utils/interactive.js +32 -0
  277. package/dist/utils/item-discovery.d.ts +19 -0
  278. package/dist/utils/item-discovery.js +30 -48
  279. package/dist/utils/shell-detection.d.ts +1 -1
  280. package/dist/utils/shell-detection.js +64 -11
  281. package/dist/utils/spec-discovery.d.ts +40 -0
  282. package/dist/utils/spec-discovery.js +115 -0
  283. package/dist/utils/task-progress.d.ts +29 -1
  284. package/dist/utils/task-progress.js +109 -15
  285. package/package.json +96 -84
  286. package/schemas/spec-driven/schema.yaml +252 -184
  287. package/schemas/spec-driven/templates/design.md +19 -19
  288. package/schemas/spec-driven/templates/plan-template.md +91 -0
  289. package/schemas/spec-driven/templates/proposal.md +29 -24
  290. package/schemas/spec-driven/templates/spec.md +11 -8
  291. package/schemas/spec-driven/templates/tasks.md +9 -9
  292. package/scripts/postinstall.js +83 -83
  293. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  294. package/dist/core/command-generation/adapters/codex.js +0 -39
  295. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  296. package/dist/core/command-generation/adapters/windsurf.js +0 -38
  297. package/dist/core/comprehension/config.d.ts +0 -20
  298. package/dist/core/comprehension/config.js +0 -23
  299. package/dist/core/comprehension/fingerprint.d.ts +0 -14
  300. package/dist/core/comprehension/fingerprint.js +0 -33
  301. package/dist/core/comprehension/index.d.ts +0 -57
  302. package/dist/core/comprehension/index.js +0 -87
  303. package/dist/core/comprehension/pass-record.d.ts +0 -29
  304. package/dist/core/comprehension/pass-record.js +0 -64
  305. package/dist/core/comprehension/stats.d.ts +0 -35
  306. package/dist/core/comprehension/stats.js +0 -133
  307. package/dist/core/templates/workflows/comprehension-guidance.d.ts +0 -9
  308. package/dist/core/templates/workflows/comprehension-guidance.js +0 -73
  309. package/dist/core/templates/workflows/mcp-guidance.d.ts +0 -13
  310. package/dist/core/templates/workflows/mcp-guidance.js +0 -131
  311. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +0 -16
  312. package/dist/core/templates/workflows/user-prompt-guidance.js +0 -48
  313. package/dist/telemetry/caller.d.ts +0 -5
  314. package/dist/telemetry/caller.js +0 -29
  315. package/dist/telemetry/client.d.ts +0 -27
  316. package/dist/telemetry/client.js +0 -127
  317. package/dist/telemetry/command-context.d.ts +0 -13
  318. package/dist/telemetry/command-context.js +0 -59
  319. package/dist/telemetry/comprehension.d.ts +0 -44
  320. package/dist/telemetry/comprehension.js +0 -105
  321. package/dist/telemetry/content.d.ts +0 -10
  322. package/dist/telemetry/content.js +0 -56
  323. package/dist/telemetry/git-stats.d.ts +0 -12
  324. package/dist/telemetry/git-stats.js +0 -69
  325. package/dist/telemetry/identify-cache.d.ts +0 -7
  326. package/dist/telemetry/identify-cache.js +0 -47
  327. package/dist/telemetry/input.d.ts +0 -17
  328. package/dist/telemetry/input.js +0 -68
  329. package/dist/telemetry/marker.d.ts +0 -37
  330. package/dist/telemetry/marker.js +0 -67
  331. package/dist/telemetry/workflow.d.ts +0 -85
  332. package/dist/telemetry/workflow.js +0 -318
  333. package/schemas/spec-driven/templates/plan.md +0 -19
@@ -3,7 +3,12 @@ import * as fs from 'fs';
3
3
  import { AI_TOOLS } from './config.js';
4
4
  import { ALL_WORKFLOWS } from './profiles.js';
5
5
  import { CommandAdapterRegistry } from './command-generation/index.js';
6
- import { COMMAND_IDS, getConfiguredTools } from './shared/index.js';
6
+ import { getConfiguredTools } from './shared/index.js';
7
+ import { shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
8
+ import { readSharedSkillTarget } from './shared-skill-target.js';
9
+ import { FileSystemUtils } from '../utils/file-system.js';
10
+ import { isLegacyCodexSkillEquivalentToCurrent } from './shared/skill-content-equivalence.js';
11
+ import { hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/skill-paths.js';
7
12
  /**
8
13
  * Maps workflow IDs to their skill directory names.
9
14
  */
@@ -11,8 +16,8 @@ export const WORKFLOW_TO_SKILL_DIR = {
11
16
  'explore': 'openspec-explore',
12
17
  'new': 'openspec-new-change',
13
18
  'continue': 'openspec-continue-change',
14
- 'modify': 'openspec-modify-change',
15
19
  'apply': 'openspec-apply-change',
20
+ 'update': 'openspec-update-change',
16
21
  'ff': 'openspec-ff-change',
17
22
  'sync': 'openspec-sync-specs',
18
23
  'archive': 'openspec-archive-change',
@@ -24,48 +29,11 @@ export const WORKFLOW_TO_SKILL_DIR = {
24
29
  function toKnownWorkflows(workflows) {
25
30
  return workflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
26
31
  }
27
- /**
28
- * Checks whether a tool has at least one generated OpenSpec command file.
29
- */
30
- export function toolHasAnyConfiguredCommand(projectPath, toolId) {
31
- const adapter = CommandAdapterRegistry.get(toolId);
32
- if (!adapter)
33
- return false;
34
- for (const commandId of COMMAND_IDS) {
35
- const cmdPath = adapter.getFilePath(commandId);
36
- const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
37
- if (fs.existsSync(fullPath)) {
38
- return true;
39
- }
40
- }
41
- return false;
42
- }
43
- /**
44
- * Returns tools with at least one generated command file on disk.
45
- */
46
- export function getCommandConfiguredTools(projectPath) {
47
- return AI_TOOLS
48
- .filter((tool) => {
49
- if (!tool.skillsDir)
50
- return false;
51
- const toolDir = path.join(projectPath, tool.skillsDir);
52
- try {
53
- return fs.statSync(toolDir).isDirectory();
54
- }
55
- catch {
56
- return false;
57
- }
58
- })
59
- .map((tool) => tool.value)
60
- .filter((toolId) => toolHasAnyConfiguredCommand(projectPath, toolId));
61
- }
62
32
  /**
63
33
  * Returns tools that are configured via either skills or commands.
64
34
  */
65
35
  export function getConfiguredToolsForProfileSync(projectPath) {
66
- const skillConfigured = getConfiguredTools(projectPath);
67
- const commandConfigured = getCommandConfiguredTools(projectPath);
68
- return [...new Set([...skillConfigured, ...commandConfigured])];
36
+ return getConfiguredTools(projectPath);
69
37
  }
70
38
  /**
71
39
  * Detects if a single tool has profile/delivery drift against the desired state.
@@ -77,14 +45,45 @@ export function getConfiguredToolsForProfileSync(projectPath) {
77
45
  */
78
46
  export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkflows, delivery) {
79
47
  const tool = AI_TOOLS.find((t) => t.value === toolId);
80
- if (!tool?.skillsDir)
48
+ if (!tool || !toolSupportsSkills(tool))
81
49
  return false;
82
50
  const knownDesiredWorkflows = toKnownWorkflows(desiredWorkflows);
83
51
  const desiredWorkflowSet = new Set(knownDesiredWorkflows);
84
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
52
+ const skillsDir = resolveToolSkillsDir(projectPath, tool);
85
53
  const adapter = CommandAdapterRegistry.get(toolId);
86
- const shouldGenerateSkills = delivery !== 'commands';
87
- const shouldGenerateCommands = delivery !== 'skills';
54
+ const shouldGenerateSkills = shouldGenerateSkillsForTool(toolId, delivery);
55
+ const shouldGenerateCommands = shouldGenerateCommandsForTool(toolId, delivery);
56
+ const sharedTarget = tool.skillsDir
57
+ ? readSharedSkillTarget(projectPath, tool.skillsDir)
58
+ : undefined;
59
+ for (const root of tool.legacySkillsDirs ?? []) {
60
+ for (const workflow of knownDesiredWorkflows) {
61
+ const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
62
+ const legacySkill = path.join(projectPath, root, 'skills', dirName, 'SKILL.md');
63
+ if (!fs.existsSync(legacySkill))
64
+ continue;
65
+ const currentSkill = path.join(skillsDir, dirName, 'SKILL.md');
66
+ if (!fs.existsSync(currentSkill) || sharedTarget !== toolId) {
67
+ return true;
68
+ }
69
+ try {
70
+ if (FileSystemUtils.canonicalizeExistingPath(legacySkill) ===
71
+ FileSystemUtils.canonicalizeExistingPath(currentSkill)) {
72
+ continue;
73
+ }
74
+ // Equivalent generated copies are actionable: migration can safely
75
+ // remove the redundant legacy file even when version, line endings,
76
+ // or supported invocation syntax changed. Materially divergent copies
77
+ // stay in place without forcing an update on every run.
78
+ if (isLegacyCodexSkillEquivalentToCurrent(fs.readFileSync(legacySkill, 'utf-8'), fs.readFileSync(currentSkill, 'utf-8'))) {
79
+ return true;
80
+ }
81
+ }
82
+ catch {
83
+ return true;
84
+ }
85
+ }
86
+ }
88
87
  if (shouldGenerateSkills) {
89
88
  for (const workflow of knownDesiredWorkflows) {
90
89
  const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
@@ -104,7 +103,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
104
103
  }
105
104
  }
106
105
  }
107
- else {
106
+ else if (shouldRemoveSkillsForTool(toolId, delivery) && !hasGlobalSkillTarget(tool)) {
108
107
  for (const workflow of ALL_WORKFLOWS) {
109
108
  const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
110
109
  const skillDir = path.join(skillsDir, dirName);
@@ -132,7 +131,7 @@ export function hasToolProfileOrDeliveryDrift(projectPath, toolId, desiredWorkfl
132
131
  }
133
132
  }
134
133
  }
135
- else if (!shouldGenerateCommands && adapter) {
134
+ else if (shouldReconcileCommandFilesForTool(toolId, delivery) && adapter) {
136
135
  for (const workflow of ALL_WORKFLOWS) {
137
136
  const cmdPath = adapter.getFilePath(workflow);
138
137
  const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
@@ -152,10 +151,10 @@ export function getToolsNeedingProfileSync(projectPath, desiredWorkflows, delive
152
151
  }
153
152
  function getInstalledWorkflowsForTool(projectPath, toolId, options) {
154
153
  const tool = AI_TOOLS.find((t) => t.value === toolId);
155
- if (!tool?.skillsDir)
154
+ if (!tool || !toolSupportsSkills(tool))
156
155
  return [];
157
156
  const installed = new Set();
158
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
157
+ const skillsDir = resolveToolSkillsDir(projectPath, tool);
159
158
  if (options.includeSkills) {
160
159
  for (const workflow of ALL_WORKFLOWS) {
161
160
  const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
@@ -188,9 +187,9 @@ export function hasProjectConfigDrift(projectPath, desiredWorkflows, delivery) {
188
187
  return true;
189
188
  }
190
189
  const desiredSet = new Set(toKnownWorkflows(desiredWorkflows));
191
- const includeSkills = delivery !== 'commands';
192
- const includeCommands = delivery !== 'skills';
193
190
  for (const toolId of configuredTools) {
191
+ const includeSkills = shouldGenerateSkillsForTool(toolId, delivery);
192
+ const includeCommands = shouldGenerateCommandsForTool(toolId, delivery);
194
193
  const installed = getInstalledWorkflowsForTool(projectPath, toolId, { includeSkills, includeCommands });
195
194
  if (installed.some((workflow) => !desiredSet.has(workflow))) {
196
195
  return true;
@@ -9,11 +9,11 @@ import type { Profile } from './global-config.js';
9
9
  * Core workflows included in the 'core' profile.
10
10
  * These provide the streamlined experience for new users.
11
11
  */
12
- export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "modify", "apply", "sync", "archive"];
12
+ export declare const CORE_WORKFLOWS: readonly ["propose", "explore", "apply", "update", "sync", "archive"];
13
13
  /**
14
14
  * All available workflows in the system.
15
15
  */
16
- export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "modify", "new", "continue", "apply", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
16
+ export declare const ALL_WORKFLOWS: readonly ["propose", "explore", "new", "continue", "apply", "update", "ff", "sync", "archive", "bulk-archive", "verify", "onboard"];
17
17
  export type WorkflowId = (typeof ALL_WORKFLOWS)[number];
18
18
  export type CoreWorkflowId = (typeof CORE_WORKFLOWS)[number];
19
19
  /**
@@ -8,17 +8,17 @@
8
8
  * Core workflows included in the 'core' profile.
9
9
  * These provide the streamlined experience for new users.
10
10
  */
11
- export const CORE_WORKFLOWS = ['propose', 'explore', 'modify', 'apply', 'sync', 'archive'];
11
+ export const CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'update', 'sync', 'archive'];
12
12
  /**
13
13
  * All available workflows in the system.
14
14
  */
15
15
  export const ALL_WORKFLOWS = [
16
16
  'propose',
17
17
  'explore',
18
- 'modify',
19
18
  'new',
20
19
  'continue',
21
20
  'apply',
21
+ 'update',
22
22
  'ff',
23
23
  'sync',
24
24
  'archive',
@@ -1,4 +1,10 @@
1
1
  import { z } from 'zod';
2
+ export declare const OPERATION_IDS: readonly ["apply", "archive"];
3
+ export type OperationId = (typeof OPERATION_IDS)[number];
4
+ export interface OperationConfig {
5
+ guidance?: string[];
6
+ }
7
+ export type OperationsConfig = Partial<Record<OperationId, OperationConfig>>;
2
8
  /**
3
9
  * Zod schema for project configuration.
4
10
  *
@@ -16,12 +22,17 @@ export declare const ProjectConfigSchema: z.ZodObject<{
16
22
  schema: z.ZodString;
17
23
  context: z.ZodOptional<z.ZodString>;
18
24
  rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
25
+ operations: z.ZodOptional<z.ZodObject<{
26
+ apply: z.ZodOptional<z.ZodObject<{
27
+ guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
+ }, z.core.$strip>>;
29
+ archive: z.ZodOptional<z.ZodObject<{
30
+ guidance: z.ZodOptional<z.ZodArray<z.ZodString>>;
31
+ }, z.core.$strip>>;
32
+ }, z.core.$strip>>;
19
33
  store: z.ZodOptional<z.ZodString>;
20
- comprehension: z.ZodOptional<z.ZodObject<{
21
- enabled: z.ZodOptional<z.ZodBoolean>;
22
- threshold_percent: z.ZodOptional<z.ZodNumber>;
23
- min_questions: z.ZodOptional<z.ZodNumber>;
24
- max_questions: z.ZodOptional<z.ZodNumber>;
34
+ githubCopilot: z.ZodOptional<z.ZodObject<{
35
+ cloudAgent: z.ZodOptional<z.ZodBoolean>;
25
36
  }, z.core.$strip>>;
26
37
  }, z.core.$strip>;
27
38
  /** Normalized in-memory shape of a referenced store declaration. */
@@ -32,14 +43,12 @@ export interface DeclarationEntry {
32
43
  }
33
44
  export type ProjectConfig = z.infer<typeof ProjectConfigSchema> & {
34
45
  references?: DeclarationEntry[];
35
- /** Normalized comprehension settings (camelCase in memory). */
36
- comprehension?: {
37
- enabled?: boolean;
38
- thresholdPercent?: number;
39
- minQuestions?: number;
40
- maxQuestions?: number;
41
- };
42
46
  };
47
+ export interface OperationInputs {
48
+ context?: string;
49
+ operationGuidance?: string[];
50
+ }
51
+ export declare function loadOperationInputs(projectConfig: ProjectConfig | null, operationId: OperationId): OperationInputs;
43
52
  export declare const MAX_CONTEXT_SIZE: number;
44
53
  /**
45
54
  * Read and parse openspec/config.yaml from project root.
@@ -62,16 +71,16 @@ export declare const MAX_CONTEXT_SIZE: number;
62
71
  */
63
72
  export declare function readProjectConfig(projectRoot: string): ProjectConfig | null;
64
73
  /**
65
- * Validate artifact IDs in rules against a schema's artifacts.
66
- * Called during instruction loading (when schema is known).
67
- * Returns warnings for unknown artifact IDs.
74
+ * Validate artifact IDs in rules against the artifacts of every available
75
+ * schema. The `rules:` map is global, but each change can use a different
76
+ * schema, so a key is only unknown when it matches no artifact in ANY schema.
77
+ * Returns warnings for keys that are unknown everywhere.
68
78
  *
69
79
  * @param rules - The rules object from config
70
- * @param validArtifactIds - Set of valid artifact IDs from the schema
71
- * @param schemaName - Name of the schema for error messages
80
+ * @param validArtifactIds - Set of valid artifact IDs across all schemas
72
81
  * @returns Array of warning messages for unknown artifact IDs
73
82
  */
74
- export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>, schemaName: string): string[];
83
+ export declare function validateConfigRules(rules: Record<string, string[]>, validArtifactIds: Set<string>): string[];
75
84
  /**
76
85
  * Suggest valid schema names when user provides invalid schema.
77
86
  * Uses fuzzy matching to find similar names.
@@ -2,6 +2,10 @@ import { existsSync, readFileSync, statSync } from 'fs';
2
2
  import path from 'path';
3
3
  import { parse as parseYaml } from 'yaml';
4
4
  import { z } from 'zod';
5
+ export const OPERATION_IDS = ['apply', 'archive'];
6
+ const OperationConfigSchema = z.object({
7
+ guidance: z.array(z.string()).optional(),
8
+ });
5
9
  /**
6
10
  * Zod schema for project configuration.
7
11
  *
@@ -34,6 +38,14 @@ export const ProjectConfigSchema = z.object({
34
38
  )
35
39
  .optional()
36
40
  .describe('Per-artifact rules, keyed by artifact ID'),
41
+ // Optional: per-operation advisory guidance, kept separate from artifact rules.
42
+ operations: z
43
+ .object({
44
+ apply: OperationConfigSchema.optional(),
45
+ archive: OperationConfigSchema.optional(),
46
+ })
47
+ .optional()
48
+ .describe('Per-operation advisory guidance'),
37
49
  // Note: the `references` field (id strings or {id, remote} maps) is
38
50
  // deliberately absent here — readProjectConfig parses and normalizes
39
51
  // it by hand (see DeclarationEntry below); a schema entry nothing
@@ -45,17 +57,70 @@ export const ProjectConfigSchema = z.object({
45
57
  .string()
46
58
  .optional()
47
59
  .describe('Store id used as the OpenSpec root when no local planning shape exists'),
48
- // Optional: apply comprehension quiz gate (default enabled when absent)
49
- comprehension: z
60
+ // Optional: GitHub Copilot integration preferences. `cloudAgent` is the
61
+ // opt-in for generating the Copilot cloud coding-agent files (a GitHub
62
+ // Actions workflow + agent file); absent means "not yet decided".
63
+ githubCopilot: z
50
64
  .object({
51
- enabled: z.boolean().optional(),
52
- threshold_percent: z.number().int().min(0).max(100).optional(),
53
- min_questions: z.number().int().positive().optional(),
54
- max_questions: z.number().int().positive().optional(),
65
+ cloudAgent: z.boolean().optional(),
55
66
  })
56
67
  .optional()
57
- .describe('Comprehension quiz gate before apply'),
68
+ .describe('GitHub Copilot integration preferences'),
58
69
  });
70
+ export function loadOperationInputs(projectConfig, operationId) {
71
+ const context = projectConfig?.context !== undefined && projectConfig.context.trim().length > 0
72
+ ? projectConfig.context
73
+ : undefined;
74
+ const guidance = projectConfig?.operations?.[operationId]?.guidance;
75
+ const operationGuidance = guidance && guidance.length > 0 ? guidance : undefined;
76
+ return {
77
+ ...(context !== undefined ? { context } : {}),
78
+ ...(operationGuidance !== undefined ? { operationGuidance } : {}),
79
+ };
80
+ }
81
+ function parseOperations(raw) {
82
+ if (raw === undefined) {
83
+ return undefined;
84
+ }
85
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
86
+ console.warn(`Invalid 'operations' field in config (must be object)`);
87
+ return undefined;
88
+ }
89
+ const supported = new Set(OPERATION_IDS);
90
+ const operations = {};
91
+ for (const [operationId, value] of Object.entries(raw)) {
92
+ if (!supported.has(operationId)) {
93
+ console.warn(`Unknown operation ID '${operationId}' in config. Supported operation IDs: ${OPERATION_IDS.join(', ')}`);
94
+ continue;
95
+ }
96
+ const typedOperationId = operationId;
97
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
98
+ console.warn(`Invalid 'operations.${operationId}' field in config (must be object), ignoring this operation`);
99
+ continue;
100
+ }
101
+ const operation = value;
102
+ const unknownFields = Object.keys(operation).filter((field) => field !== 'guidance');
103
+ if (unknownFields.length > 0) {
104
+ console.warn(`Unknown field(s) in 'operations.${operationId}': ${unknownFields.join(', ')}. Supported fields: guidance`);
105
+ }
106
+ if (operation.guidance === undefined) {
107
+ continue;
108
+ }
109
+ const guidanceResult = z.array(z.string()).safeParse(operation.guidance);
110
+ if (!guidanceResult.success) {
111
+ console.warn(`Guidance for operation '${operationId}' must be an array of strings, ignoring this operation's guidance`);
112
+ continue;
113
+ }
114
+ const guidance = guidanceResult.data.filter((entry) => entry.length > 0);
115
+ if (guidance.length < guidanceResult.data.length) {
116
+ console.warn(`Some guidance for operation '${operationId}' are empty strings, ignoring them`);
117
+ }
118
+ if (guidance.length > 0) {
119
+ operations[typedOperationId] = { guidance };
120
+ }
121
+ }
122
+ return Object.keys(operations).length > 0 ? operations : undefined;
123
+ }
59
124
  /**
60
125
  * Parser for `references:` declarations: string entries or
61
126
  * {id, remote} maps, normalized to DeclarationEntry[]. Dedup keys on
@@ -179,7 +244,11 @@ export function readProjectConfig(projectRoot) {
179
244
  const rulesField = z.record(z.string(), z.array(z.string()));
180
245
  // First check if it's an object structure (guard against null since typeof null === 'object')
181
246
  if (typeof raw.rules === 'object' && raw.rules !== null && !Array.isArray(raw.rules)) {
182
- const parsedRules = {};
247
+ // Artifact ids are intentionally not restricted to the built-in naming
248
+ // convention, so keys such as "constructor" remain valid for custom
249
+ // schemas. A null-prototype map preserves those keys as data without
250
+ // letting "__proto__" mutate the lookup object's prototype.
251
+ const parsedRules = Object.create(null);
183
252
  let hasValidRules = false;
184
253
  for (const [artifactId, rules] of Object.entries(raw.rules)) {
185
254
  const rulesArrayResult = z.array(z.string()).safeParse(rules);
@@ -206,6 +275,10 @@ export function readProjectConfig(projectRoot) {
206
275
  console.warn(`Invalid 'rules' field in config (must be object)`);
207
276
  }
208
277
  }
278
+ const operations = parseOperations(raw.operations);
279
+ if (operations) {
280
+ config.operations = operations;
281
+ }
209
282
  const references = parseDeclarationList(raw.references);
210
283
  if (references) {
211
284
  config.references = references;
@@ -221,27 +294,21 @@ export function readProjectConfig(projectRoot) {
221
294
  console.warn(`Warning: ignoring invalid store: field in ${configPathForWarnings(projectRoot)} (must be a single store id string).`);
222
295
  }
223
296
  }
224
- if (raw.comprehension !== undefined) {
225
- const comprehensionField = z.object({
226
- enabled: z.boolean().optional(),
227
- threshold_percent: z.number().int().min(0).max(100).optional(),
228
- min_questions: z.number().int().positive().optional(),
229
- max_questions: z.number().int().positive().optional(),
230
- });
231
- const comprehensionResult = comprehensionField.safeParse(raw.comprehension);
232
- if (comprehensionResult.success) {
233
- const c = comprehensionResult.data;
234
- config.comprehension = {
235
- ...(c.enabled !== undefined ? { enabled: c.enabled } : {}),
236
- ...(c.threshold_percent !== undefined
237
- ? { thresholdPercent: c.threshold_percent }
238
- : {}),
239
- ...(c.min_questions !== undefined ? { minQuestions: c.min_questions } : {}),
240
- ...(c.max_questions !== undefined ? { maxQuestions: c.max_questions } : {}),
241
- };
297
+ // Parse githubCopilot preferences (only cloudAgent is recognized today).
298
+ if (raw.githubCopilot !== undefined) {
299
+ if (typeof raw.githubCopilot === 'object' &&
300
+ raw.githubCopilot !== null &&
301
+ !Array.isArray(raw.githubCopilot)) {
302
+ const cloudAgent = raw.githubCopilot.cloudAgent;
303
+ if (typeof cloudAgent === 'boolean') {
304
+ config.githubCopilot = { cloudAgent };
305
+ }
306
+ else if (cloudAgent !== undefined) {
307
+ console.warn(`Invalid 'githubCopilot.cloudAgent' field in config (must be a boolean)`);
308
+ }
242
309
  }
243
310
  else {
244
- console.warn(`Invalid 'comprehension' field in config (must be an object)`);
311
+ console.warn(`Invalid 'githubCopilot' field in config (must be an object)`);
245
312
  }
246
313
  }
247
314
  // Return partial config even if some fields failed
@@ -256,22 +323,22 @@ function configPathForWarnings(projectRoot) {
256
323
  return resolveConfigFilePath(projectRoot) ?? path.join(projectRoot, 'openspec', 'config.yaml');
257
324
  }
258
325
  /**
259
- * Validate artifact IDs in rules against a schema's artifacts.
260
- * Called during instruction loading (when schema is known).
261
- * Returns warnings for unknown artifact IDs.
326
+ * Validate artifact IDs in rules against the artifacts of every available
327
+ * schema. The `rules:` map is global, but each change can use a different
328
+ * schema, so a key is only unknown when it matches no artifact in ANY schema.
329
+ * Returns warnings for keys that are unknown everywhere.
262
330
  *
263
331
  * @param rules - The rules object from config
264
- * @param validArtifactIds - Set of valid artifact IDs from the schema
265
- * @param schemaName - Name of the schema for error messages
332
+ * @param validArtifactIds - Set of valid artifact IDs across all schemas
266
333
  * @returns Array of warning messages for unknown artifact IDs
267
334
  */
268
- export function validateConfigRules(rules, validArtifactIds, schemaName) {
335
+ export function validateConfigRules(rules, validArtifactIds) {
269
336
  const warnings = [];
270
337
  for (const artifactId of Object.keys(rules)) {
271
338
  if (!validArtifactIds.has(artifactId)) {
272
339
  const validIds = Array.from(validArtifactIds).sort().join(', ');
273
340
  warnings.push(`Unknown artifact ID in rules: "${artifactId}". ` +
274
- `Valid IDs for schema "${schemaName}": ${validIds}`);
341
+ `It matches no artifact in any available schema. Known artifact IDs: ${validIds}`);
275
342
  }
276
343
  }
277
344
  return warnings;
@@ -53,6 +53,46 @@ function registerFix(id, remote) {
53
53
  }
54
54
  return `Get a checkout from a teammate and run: openspec store register <path> --id ${id}`;
55
55
  }
56
+ const WHITESPACE = /\s/;
57
+ /**
58
+ * Drop a CommonMark closing sequence (`## Purpose ##`). The closing run only
59
+ * counts when whitespace precedes it, so `Purpose###` keeps its hashes.
60
+ * Scans from the end so the cost stays linear in the title length.
61
+ */
62
+ function stripClosingSequence(title) {
63
+ let end = title.length;
64
+ while (end > 0 && WHITESPACE.test(title[end - 1])) {
65
+ end--;
66
+ }
67
+ const hashEnd = end;
68
+ while (end > 0 && title[end - 1] === '#') {
69
+ end--;
70
+ }
71
+ const noClosingRun = end === hashEnd;
72
+ const missingLeadingSpace = end === 0 || !WHITESPACE.test(title[end - 1]);
73
+ if (noClosingRun || missingLeadingSpace) {
74
+ return title.trim();
75
+ }
76
+ return title.slice(0, end).trim();
77
+ }
78
+ /**
79
+ * Heading title, or null when the line is not an ATX heading. Hand-rolled
80
+ * rather than a regex so a title padded with whitespace cannot backtrack.
81
+ */
82
+ function parseHeadingTitle(line) {
83
+ let level = 0;
84
+ while (level < 6 && level < line.length && line[level] === '#') {
85
+ level++;
86
+ }
87
+ if (level === 0 || level >= line.length || !WHITESPACE.test(line[level])) {
88
+ return null;
89
+ }
90
+ let start = level;
91
+ while (start < line.length && WHITESPACE.test(line[start])) {
92
+ start++;
93
+ }
94
+ return stripClosingSequence(line.slice(start));
95
+ }
56
96
  /**
57
97
  * Tolerant first-Purpose-line extraction. parseSpec() throws on specs
58
98
  * without Purpose/Requirements sections; the index must never fail on an
@@ -79,12 +119,11 @@ export function extractFirstPurposeLine(markdown) {
79
119
  if (fenceMarker !== null) {
80
120
  continue;
81
121
  }
82
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
83
- if (heading) {
122
+ const title = parseHeadingTitle(line);
123
+ if (title !== null) {
84
124
  if (inPurpose) {
85
125
  return '';
86
126
  }
87
- const title = heading[2].replace(/\s+#+\s*$/, '').trim();
88
127
  inPurpose = title.toLowerCase() === 'purpose';
89
128
  continue;
90
129
  }
@@ -152,7 +191,6 @@ export function renderReferencedStoresSection(entries) {
152
191
  * let hostile content forge instruction lines (slice 6.1 hardening).
153
192
  */
154
193
  export function sanitizeInline(value, maxLength = 300) {
155
- // eslint-disable-next-line no-control-regex
156
194
  const flattened = value.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
157
195
  return flattened.length > maxLength ? `${flattened.slice(0, maxLength)}…` : flattened;
158
196
  }
@@ -26,6 +26,10 @@ export interface RelationshipHealth {
26
26
  remote?: string;
27
27
  };
28
28
  origin_url?: string;
29
+ drift?: {
30
+ ahead: number;
31
+ behind: number;
32
+ };
29
33
  status: StoreDiagnostic[];
30
34
  } | null;
31
35
  references: ReferenceIndexEntry[];
@@ -42,6 +46,10 @@ export interface InspectRelationshipsInput {
42
46
  metadataValid: boolean;
43
47
  canonicalRemote?: string;
44
48
  originUrl?: string;
49
+ drift?: {
50
+ ahead: number;
51
+ behind: number;
52
+ };
45
53
  };
46
54
  referenceEntries: ReferenceIndexEntry[];
47
55
  registryUnreadable: boolean;
@@ -37,6 +37,18 @@ export function inspectRelationships(input) {
37
37
  input.storeFacts.canonicalRemote !== input.storeFacts.originUrl) {
38
38
  storeStatus.push(makeStoreDiagnostic('info', 'store_remote_divergence', `The store.yaml remote (${sanitizeInline(input.storeFacts.canonicalRemote, 200)}) differs from the checkout's origin (${sanitizeInline(input.storeFacts.originUrl, 200)}).`, { target: 'store.metadata' }));
39
39
  }
40
+ // Checkout behind its upstream tracking ref: a read-only staleness
41
+ // signal, not a version pin — OpenSpec never syncs stores, so this
42
+ // compares against the local upstream ref, not the live remote.
43
+ // Behind means teammates on newer commits may resolve different specs.
44
+ // Ahead-only is normal (OpenSpec never pushes stores), so it stays quiet.
45
+ const drift = input.storeFacts.drift;
46
+ if (drift && drift.behind > 0) {
47
+ const behindCommits = `${drift.behind} commit${drift.behind === 1 ? '' : 's'}`;
48
+ storeStatus.push(makeStoreDiagnostic('info', 'store_checkout_drift', drift.ahead > 0
49
+ ? `This store checkout has diverged from its upstream tracking branch (${drift.behind} behind, ${drift.ahead} ahead); teammates on newer commits may resolve different specs.`
50
+ : `This store checkout is ${behindCommits} behind its upstream tracking branch; teammates on newer commits may resolve different specs.`, { target: 'store.git' }));
51
+ }
40
52
  store = {
41
53
  id: input.storeFacts.id,
42
54
  metadata: {
@@ -47,6 +59,7 @@ export function inspectRelationships(input) {
47
59
  : {}),
48
60
  },
49
61
  ...(input.storeFacts.originUrl ? { origin_url: input.storeFacts.originUrl } : {}),
62
+ ...(drift ? { drift } : {}),
50
63
  status: storeStatus,
51
64
  };
52
65
  }
@@ -7,8 +7,11 @@
7
7
  * - `--store <id>` selects a registered store's root.
8
8
  * - Without `--store`, the nearest ancestor containing `openspec/` wins.
9
9
  * Leftover workspace view state is never considered a root here.
10
- * - With no nearest root, registered stores produce a selection hint error;
11
- * otherwise commands may treat the current directory as an implicit root.
10
+ * - With no nearest root, a global `defaultStore` (if set) is the last
11
+ * machine-level fallback before the selection hint error.
12
+ * - With no nearest root and no default, registered stores produce a
13
+ * selection hint error; otherwise commands may treat the current
14
+ * directory as an implicit root.
12
15
  *
13
16
  * Diagnostic codes reuse the store taxonomy where an error passes
14
17
  * through unchanged (`invalid_store_id`, metadata parse failures);
@@ -19,7 +22,7 @@
19
22
  * `no_openspec_root`).
20
23
  */
21
24
  import { type PlanningHome } from './planning-home.js';
22
- export type OpenSpecRootSource = 'store' | 'declared' | 'nearest' | 'implicit';
25
+ export type OpenSpecRootSource = 'store' | 'declared' | 'global_default' | 'nearest' | 'implicit';
23
26
  export interface StoreSelectorOptions {
24
27
  store?: string;
25
28
  storePath?: string;
@@ -84,9 +87,9 @@ export interface RootOutput {
84
87
  }
85
88
  export declare function toRootOutput(root: ResolvedOpenSpecRoot): RootOutput;
86
89
  /**
87
- * A store-selected root — explicit `--store` or the declared fallback.
88
- * Cross-root behavior (absolute paths, --store hints, suppressed
89
- * noun-form suggestions) keys on this, never on `source` directly.
90
+ * A store-selected root — explicit `--store`, a declared pointer, or the
91
+ * global default. Cross-root behavior (absolute paths, --store hints,
92
+ * suppressed noun-form suggestions) keys on this, never on `source` directly.
90
93
  */
91
94
  export declare function isStoreSelectedRoot(root: ResolvedOpenSpecRoot): root is ResolvedOpenSpecRoot & {
92
95
  storeId: string;