@fission-ai/openspec 1.4.1 → 1.6.0-beta.1

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 (241) hide show
  1. package/README.md +37 -4
  2. package/dist/cli/index.d.ts +5 -0
  3. package/dist/cli/index.js +95 -95
  4. package/dist/commands/change.d.ts +5 -2
  5. package/dist/commands/change.js +23 -55
  6. package/dist/commands/config.js +6 -65
  7. package/dist/commands/context.d.ts +3 -0
  8. package/dist/commands/context.js +155 -0
  9. package/dist/commands/doctor.d.ts +8 -0
  10. package/dist/commands/doctor.js +163 -0
  11. package/dist/commands/shared-gather.d.ts +14 -0
  12. package/dist/commands/shared-gather.js +31 -0
  13. package/dist/commands/shared-output.d.ts +18 -0
  14. package/dist/commands/shared-output.js +61 -0
  15. package/dist/commands/show.d.ts +11 -6
  16. package/dist/commands/show.js +70 -25
  17. package/dist/commands/spec.d.ts +5 -1
  18. package/dist/commands/spec.js +15 -4
  19. package/dist/commands/store.d.ts +3 -0
  20. package/dist/commands/store.js +547 -0
  21. package/dist/commands/validate.d.ts +10 -0
  22. package/dist/commands/validate.js +88 -40
  23. package/dist/commands/workflow/index.d.ts +0 -2
  24. package/dist/commands/workflow/index.js +0 -1
  25. package/dist/commands/workflow/instructions.d.ts +11 -1
  26. package/dist/commands/workflow/instructions.js +71 -22
  27. package/dist/commands/workflow/new-change.d.ts +6 -3
  28. package/dist/commands/workflow/new-change.js +42 -77
  29. package/dist/commands/workflow/shared.d.ts +15 -3
  30. package/dist/commands/workflow/shared.js +20 -3
  31. package/dist/commands/workflow/status.d.ts +2 -0
  32. package/dist/commands/workflow/status.js +19 -17
  33. package/dist/commands/workset-input.d.ts +19 -0
  34. package/dist/commands/workset-input.js +112 -0
  35. package/dist/commands/workset-prompts.d.ts +12 -0
  36. package/dist/commands/workset-prompts.js +143 -0
  37. package/dist/commands/workset.d.ts +25 -0
  38. package/dist/commands/workset.js +446 -0
  39. package/dist/core/archive.d.ts +17 -6
  40. package/dist/core/archive.js +220 -68
  41. package/dist/core/artifact-graph/index.d.ts +1 -1
  42. package/dist/core/artifact-graph/instruction-loader.d.ts +18 -13
  43. package/dist/core/artifact-graph/instruction-loader.js +8 -31
  44. package/dist/core/change-metadata/schema.d.ts +1 -0
  45. package/dist/core/change-metadata/schema.js +4 -2
  46. package/dist/core/change-status-policy.d.ts +5 -18
  47. package/dist/core/change-status-policy.js +2 -37
  48. package/dist/core/command-generation/adapters/bob.js +1 -14
  49. package/dist/core/command-generation/adapters/claude.d.ts +1 -1
  50. package/dist/core/command-generation/adapters/claude.js +4 -15
  51. package/dist/core/command-generation/adapters/cursor.js +1 -14
  52. package/dist/core/command-generation/adapters/index.d.ts +2 -0
  53. package/dist/core/command-generation/adapters/index.js +2 -0
  54. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
  55. package/dist/core/command-generation/adapters/oh-my-pi.js +44 -0
  56. package/dist/core/command-generation/adapters/pi.js +1 -14
  57. package/dist/core/command-generation/adapters/trae.d.ts +13 -0
  58. package/dist/core/command-generation/adapters/trae.js +48 -0
  59. package/dist/core/command-generation/adapters/windsurf.js +1 -14
  60. package/dist/core/command-generation/registry.js +4 -0
  61. package/dist/core/command-generation/yaml.d.ts +22 -0
  62. package/dist/core/command-generation/yaml.js +38 -0
  63. package/dist/core/completions/command-registry.js +87 -289
  64. package/dist/core/completions/generators/zsh-generator.js +3 -1
  65. package/dist/core/completions/installers/bash-installer.js +3 -0
  66. package/dist/core/completions/installers/fish-installer.js +8 -0
  67. package/dist/core/completions/installers/powershell-installer.js +30 -2
  68. package/dist/core/completions/installers/zsh-installer.js +3 -0
  69. package/dist/core/completions/shared-flags.d.ts +1 -0
  70. package/dist/core/completions/shared-flags.js +5 -0
  71. package/dist/core/config-schema.d.ts +2 -1
  72. package/dist/core/config-schema.js +26 -0
  73. package/dist/core/config.js +1 -0
  74. package/dist/core/file-state.d.ts +36 -0
  75. package/dist/core/file-state.js +112 -0
  76. package/dist/core/global-config.d.ts +2 -0
  77. package/dist/core/id.d.ts +17 -0
  78. package/dist/core/id.js +30 -0
  79. package/dist/core/index.d.ts +3 -3
  80. package/dist/core/index.js +3 -3
  81. package/dist/core/init.js +27 -6
  82. package/dist/core/list.d.ts +2 -0
  83. package/dist/core/list.js +37 -14
  84. package/dist/core/openers.d.ts +77 -0
  85. package/dist/core/openers.js +251 -0
  86. package/dist/core/openspec-root.d.ts +45 -0
  87. package/dist/core/openspec-root.js +195 -0
  88. package/dist/core/parsers/change-parser.js +2 -1
  89. package/dist/core/parsers/markdown-parser.d.ts +0 -3
  90. package/dist/core/parsers/markdown-parser.js +5 -59
  91. package/dist/core/parsers/requirement-blocks.d.ts +11 -0
  92. package/dist/core/parsers/requirement-blocks.js +42 -8
  93. package/dist/core/parsers/requirement-text.d.ts +55 -0
  94. package/dist/core/parsers/requirement-text.js +136 -0
  95. package/dist/core/planning-home.d.ts +1 -6
  96. package/dist/core/planning-home.js +2 -43
  97. package/dist/core/profile-sync-drift.js +1 -0
  98. package/dist/core/profiles.d.ts +2 -2
  99. package/dist/core/profiles.js +2 -1
  100. package/dist/core/project-config.d.ts +44 -1
  101. package/dist/core/project-config.js +160 -10
  102. package/dist/core/references.d.ts +63 -0
  103. package/dist/core/references.js +310 -0
  104. package/dist/core/relationship-health.d.ts +65 -0
  105. package/dist/core/relationship-health.js +64 -0
  106. package/dist/core/root-selection.d.ts +122 -0
  107. package/dist/core/root-selection.js +337 -0
  108. package/dist/core/schemas/base.schema.js +7 -2
  109. package/dist/core/shared/allowed-tools.d.ts +12 -0
  110. package/dist/core/shared/allowed-tools.js +12 -0
  111. package/dist/core/shared/skill-generation.js +5 -1
  112. package/dist/core/shared/tool-detection.d.ts +2 -2
  113. package/dist/core/shared/tool-detection.js +2 -0
  114. package/dist/core/specs-apply.d.ts +6 -1
  115. package/dist/core/specs-apply.js +41 -5
  116. package/dist/core/store/errors.d.ts +20 -0
  117. package/dist/core/{context-store → store}/errors.js +3 -3
  118. package/dist/core/store/foundation.d.ts +56 -0
  119. package/dist/core/store/foundation.js +251 -0
  120. package/dist/core/store/git.d.ts +23 -0
  121. package/dist/core/store/git.js +137 -0
  122. package/dist/core/{context-store → store}/index.d.ts +0 -1
  123. package/dist/core/{context-store → store}/index.js +0 -1
  124. package/dist/core/store/operations.d.ts +114 -0
  125. package/dist/core/store/operations.js +803 -0
  126. package/dist/core/store/registry.d.ts +58 -0
  127. package/dist/core/store/registry.js +275 -0
  128. package/dist/core/templates/skill-templates.d.ts +1 -0
  129. package/dist/core/templates/skill-templates.js +1 -0
  130. package/dist/core/templates/workflows/apply-change.js +5 -4
  131. package/dist/core/templates/workflows/archive-change.js +5 -4
  132. package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
  133. package/dist/core/templates/workflows/continue-change.js +7 -2
  134. package/dist/core/templates/workflows/explore.js +5 -0
  135. package/dist/core/templates/workflows/ff-change.js +5 -0
  136. package/dist/core/templates/workflows/new-change.js +5 -0
  137. package/dist/core/templates/workflows/onboard.js +4 -1
  138. package/dist/core/templates/workflows/propose.js +5 -0
  139. package/dist/core/templates/workflows/store-selection.d.ts +8 -0
  140. package/dist/core/templates/workflows/store-selection.js +8 -0
  141. package/dist/core/templates/workflows/sync-specs.js +5 -4
  142. package/dist/core/templates/workflows/update-change.d.ts +10 -0
  143. package/dist/core/templates/workflows/update-change.js +167 -0
  144. package/dist/core/templates/workflows/verify-change.js +5 -4
  145. package/dist/core/update.js +4 -4
  146. package/dist/core/validation/validator.d.ts +7 -0
  147. package/dist/core/validation/validator.js +101 -38
  148. package/dist/core/view.js +1 -1
  149. package/dist/core/working-set.d.ts +47 -0
  150. package/dist/core/working-set.js +43 -0
  151. package/dist/core/worksets.d.ts +75 -0
  152. package/dist/core/worksets.js +245 -0
  153. package/dist/core/zod-issues.d.ts +4 -0
  154. package/dist/core/zod-issues.js +10 -0
  155. package/dist/utils/change-metadata.d.ts +1 -0
  156. package/dist/utils/change-metadata.js +1 -1
  157. package/dist/utils/change-utils.js +15 -0
  158. package/dist/utils/file-system.js +36 -17
  159. package/dist/utils/task-progress.d.ts +10 -1
  160. package/dist/utils/task-progress.js +66 -2
  161. package/package.json +26 -23
  162. package/dist/commands/context-store.d.ts +0 -3
  163. package/dist/commands/context-store.js +0 -475
  164. package/dist/commands/initiative.d.ts +0 -13
  165. package/dist/commands/initiative.js +0 -318
  166. package/dist/commands/workflow/initiative-link.d.ts +0 -24
  167. package/dist/commands/workflow/initiative-link.js +0 -47
  168. package/dist/commands/workflow/set-change.d.ts +0 -13
  169. package/dist/commands/workflow/set-change.js +0 -87
  170. package/dist/commands/workspace/context-status.d.ts +0 -4
  171. package/dist/commands/workspace/context-status.js +0 -59
  172. package/dist/commands/workspace/open-target-selection.d.ts +0 -13
  173. package/dist/commands/workspace/open-target-selection.js +0 -146
  174. package/dist/commands/workspace/open-view.d.ts +0 -62
  175. package/dist/commands/workspace/open-view.js +0 -249
  176. package/dist/commands/workspace/open.d.ts +0 -37
  177. package/dist/commands/workspace/open.js +0 -110
  178. package/dist/commands/workspace/opener-selection.d.ts +0 -11
  179. package/dist/commands/workspace/opener-selection.js +0 -98
  180. package/dist/commands/workspace/operations.d.ts +0 -29
  181. package/dist/commands/workspace/operations.js +0 -543
  182. package/dist/commands/workspace/prompt-theme.d.ts +0 -29
  183. package/dist/commands/workspace/prompt-theme.js +0 -24
  184. package/dist/commands/workspace/registration.d.ts +0 -13
  185. package/dist/commands/workspace/registration.js +0 -84
  186. package/dist/commands/workspace/selection.d.ts +0 -8
  187. package/dist/commands/workspace/selection.js +0 -104
  188. package/dist/commands/workspace/setup-prompts.d.ts +0 -13
  189. package/dist/commands/workspace/setup-prompts.js +0 -121
  190. package/dist/commands/workspace/types.d.ts +0 -103
  191. package/dist/commands/workspace/types.js +0 -36
  192. package/dist/commands/workspace.d.ts +0 -5
  193. package/dist/commands/workspace.js +0 -574
  194. package/dist/core/collections/index.d.ts +0 -3
  195. package/dist/core/collections/index.js +0 -3
  196. package/dist/core/collections/initiatives/collection.d.ts +0 -4
  197. package/dist/core/collections/initiatives/collection.js +0 -17
  198. package/dist/core/collections/initiatives/index.d.ts +0 -6
  199. package/dist/core/collections/initiatives/index.js +0 -6
  200. package/dist/core/collections/initiatives/operations.d.ts +0 -49
  201. package/dist/core/collections/initiatives/operations.js +0 -175
  202. package/dist/core/collections/initiatives/resolution.d.ts +0 -87
  203. package/dist/core/collections/initiatives/resolution.js +0 -374
  204. package/dist/core/collections/initiatives/schema.d.ts +0 -41
  205. package/dist/core/collections/initiatives/schema.js +0 -134
  206. package/dist/core/collections/initiatives/templates.d.ts +0 -12
  207. package/dist/core/collections/initiatives/templates.js +0 -90
  208. package/dist/core/collections/runtime.d.ts +0 -46
  209. package/dist/core/collections/runtime.js +0 -194
  210. package/dist/core/context-store/binding.d.ts +0 -53
  211. package/dist/core/context-store/binding.js +0 -197
  212. package/dist/core/context-store/errors.d.ts +0 -20
  213. package/dist/core/context-store/foundation.d.ts +0 -55
  214. package/dist/core/context-store/foundation.js +0 -321
  215. package/dist/core/context-store/operations.d.ts +0 -85
  216. package/dist/core/context-store/operations.js +0 -528
  217. package/dist/core/context-store/registry.d.ts +0 -45
  218. package/dist/core/context-store/registry.js +0 -229
  219. package/dist/core/workspace/foundation.d.ts +0 -67
  220. package/dist/core/workspace/foundation.js +0 -295
  221. package/dist/core/workspace/index.d.ts +0 -8
  222. package/dist/core/workspace/index.js +0 -8
  223. package/dist/core/workspace/legacy-state.d.ts +0 -28
  224. package/dist/core/workspace/legacy-state.js +0 -200
  225. package/dist/core/workspace/link-input.d.ts +0 -9
  226. package/dist/core/workspace/link-input.js +0 -32
  227. package/dist/core/workspace/open-surface.d.ts +0 -45
  228. package/dist/core/workspace/open-surface.js +0 -215
  229. package/dist/core/workspace/openers.d.ts +0 -21
  230. package/dist/core/workspace/openers.js +0 -124
  231. package/dist/core/workspace/registry.d.ts +0 -24
  232. package/dist/core/workspace/registry.js +0 -146
  233. package/dist/core/workspace/skills.d.ts +0 -57
  234. package/dist/core/workspace/skills.js +0 -334
  235. package/dist/core/workspace/state-io.d.ts +0 -10
  236. package/dist/core/workspace/state-io.js +0 -121
  237. package/schemas/workspace-planning/schema.yaml +0 -72
  238. package/schemas/workspace-planning/templates/design.md +0 -33
  239. package/schemas/workspace-planning/templates/proposal.md +0 -28
  240. package/schemas/workspace-planning/templates/spec.md +0 -9
  241. package/schemas/workspace-planning/templates/tasks.md +0 -15
@@ -1,15 +1,21 @@
1
1
  import ora from 'ora';
2
2
  import path from 'path';
3
3
  import { Validator } from '../core/validation/validator.js';
4
+ import { resolveRootForCommand, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../core/root-selection.js';
4
5
  import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
5
- import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
6
+ import { getSpecIds } from '../utils/item-discovery.js';
7
+ import { getAvailableChanges } from './workflow/shared.js';
6
8
  import { nearestMatches } from '../utils/match.js';
7
9
  export class ValidateCommand {
8
10
  async execute(itemName, options = {}) {
11
+ const root = await resolveRootForCommand(options, { json: options.json });
12
+ if (!root) {
13
+ return;
14
+ }
9
15
  const interactive = isInteractive(options);
10
16
  // Handle bulk flags first
11
17
  if (options.all || options.changes || options.specs) {
12
- await this.runBulkValidation({
18
+ await this.runBulkValidation(root, {
13
19
  changes: !!options.all || !!options.changes,
14
20
  specs: !!options.all || !!options.specs,
15
21
  }, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
@@ -18,16 +24,16 @@ export class ValidateCommand {
18
24
  // No item and no flags
19
25
  if (!itemName) {
20
26
  if (interactive) {
21
- await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
27
+ await this.runInteractiveSelector(root, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
22
28
  return;
23
29
  }
24
- this.printNonInteractiveHint();
30
+ this.printNonInteractiveHint(root);
25
31
  process.exitCode = 1;
26
32
  return;
27
33
  }
28
34
  // Direct item validation with type detection or override
29
35
  const typeOverride = this.normalizeType(options.type);
30
- await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
36
+ await this.validateDirectItem(root, itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
31
37
  }
32
38
  normalizeType(value) {
33
39
  if (!value)
@@ -37,7 +43,18 @@ export class ValidateCommand {
37
43
  return v;
38
44
  return undefined;
39
45
  }
40
- async runInteractiveSelector(opts) {
46
+ /**
47
+ * Resolve change IDs by directory existence within the resolved root — the
48
+ * same rule `openspec status`/`instructions` use (`getAvailableChanges`) —
49
+ * rather than requiring `proposal.md`. This lets `validate` resolve a
50
+ * scaffolded or still-authoring change that the sibling commands already
51
+ * resolve (#1182). Sorted to preserve the prior `getActiveChangeIds` ordering.
52
+ */
53
+ async listChangeIds(root) {
54
+ const ids = await getAvailableChanges(root.path, root.changesDir);
55
+ return ids.sort();
56
+ }
57
+ async runInteractiveSelector(root, opts) {
41
58
  const { select } = await import('@inquirer/prompts');
42
59
  const choice = await select({
43
60
  message: 'What would you like to validate?',
@@ -49,13 +66,13 @@ export class ValidateCommand {
49
66
  ],
50
67
  });
51
68
  if (choice === 'all')
52
- return this.runBulkValidation({ changes: true, specs: true }, opts);
69
+ return this.runBulkValidation(root, { changes: true, specs: true }, opts);
53
70
  if (choice === 'changes')
54
- return this.runBulkValidation({ changes: true, specs: false }, opts);
71
+ return this.runBulkValidation(root, { changes: true, specs: false }, opts);
55
72
  if (choice === 'specs')
56
- return this.runBulkValidation({ changes: false, specs: true }, opts);
73
+ return this.runBulkValidation(root, { changes: false, specs: true }, opts);
57
74
  // one
58
- const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
75
+ const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
59
76
  const items = [];
60
77
  items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
61
78
  items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec', id } })));
@@ -65,59 +82,85 @@ export class ValidateCommand {
65
82
  return;
66
83
  }
67
84
  const picked = await select({ message: 'Pick an item', choices: items });
68
- await this.validateByType(picked.type, picked.id, opts);
85
+ await this.validateByType(root, picked.type, picked.id, opts);
69
86
  }
70
- printNonInteractiveHint() {
87
+ printNonInteractiveHint(root) {
71
88
  console.error('Nothing to validate. Try one of:');
72
- console.error(' openspec validate --all');
73
- console.error(' openspec validate --changes');
74
- console.error(' openspec validate --specs');
75
- console.error(' openspec validate <item-name>');
89
+ console.error(` ${withStoreFlag(root, 'openspec validate --all')}`);
90
+ console.error(` ${withStoreFlag(root, 'openspec validate --changes')}`);
91
+ console.error(` ${withStoreFlag(root, 'openspec validate --specs')}`);
92
+ console.error(` ${withStoreFlag(root, 'openspec validate <item-name>')}`);
76
93
  console.error('Or run in an interactive terminal.');
77
94
  }
78
- async validateDirectItem(itemName, opts) {
79
- const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
95
+ async validateDirectItem(root, itemName, opts) {
96
+ const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
80
97
  const isChange = changes.includes(itemName);
81
98
  const isSpec = specs.includes(itemName);
82
99
  const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
83
100
  if (!type) {
84
- console.error(`Unknown item '${itemName}'`);
85
101
  const suggestions = nearestMatches(itemName, [...changes, ...specs]);
86
- if (suggestions.length)
87
- console.error(`Did you mean: ${suggestions.join(', ')}?`);
102
+ const message = suggestions.length
103
+ ? `Unknown item '${itemName}'. Did you mean: ${suggestions.join(', ')}?`
104
+ : `Unknown item '${itemName}'.`;
105
+ if (opts.json) {
106
+ console.log(JSON.stringify({ status: [{ severity: 'error', code: 'unknown_item', message }] }, null, 2));
107
+ }
108
+ else {
109
+ console.error(message);
110
+ }
88
111
  process.exitCode = 1;
89
112
  return;
90
113
  }
91
114
  if (!opts.typeOverride && isChange && isSpec) {
115
+ if (opts.json) {
116
+ console.log(JSON.stringify({
117
+ status: [
118
+ {
119
+ severity: 'error',
120
+ code: 'ambiguous_item',
121
+ message: `Ambiguous item '${itemName}' matches both a change and a spec.`,
122
+ fix: 'Pass --type change|spec.',
123
+ },
124
+ ],
125
+ }, null, 2));
126
+ process.exitCode = 1;
127
+ return;
128
+ }
92
129
  console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
93
- console.error('Pass --type change|spec, or use: openspec change validate / openspec spec validate');
130
+ // The noun-form commands are cwd-based and cannot reach a selected store.
131
+ if (isStoreSelectedRoot(root)) {
132
+ console.error('Pass --type change|spec.');
133
+ }
134
+ else {
135
+ console.error('Pass --type change|spec, or use: openspec change validate / openspec spec validate');
136
+ }
94
137
  process.exitCode = 1;
95
138
  return;
96
139
  }
97
- await this.validateByType(type, itemName, opts);
140
+ await this.validateByType(root, type, itemName, opts);
98
141
  }
99
- async validateByType(type, id, opts) {
142
+ async validateByType(root, type, id, opts) {
100
143
  const validator = new Validator(opts.strict);
101
144
  if (type === 'change') {
102
- const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
145
+ const changeDir = path.join(root.changesDir, id);
103
146
  const start = Date.now();
104
147
  const report = await validator.validateChangeDeltaSpecs(changeDir);
105
148
  const durationMs = Date.now() - start;
106
- this.printReport('change', id, report, durationMs, opts.json);
149
+ this.printReport('change', id, report, durationMs, opts.json, root);
107
150
  // Non-zero exit if invalid (keeps enriched output test semantics)
108
151
  process.exitCode = report.valid ? 0 : 1;
109
152
  return;
110
153
  }
111
- const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
154
+ const file = path.join(root.specsDir, id, 'spec.md');
112
155
  const start = Date.now();
113
156
  const report = await validator.validateSpec(file);
114
157
  const durationMs = Date.now() - start;
115
- this.printReport('spec', id, report, durationMs, opts.json);
158
+ this.printReport('spec', id, report, durationMs, opts.json, root);
116
159
  process.exitCode = report.valid ? 0 : 1;
117
160
  }
118
- printReport(type, id, report, durationMs, json) {
161
+ printReport(type, id, report, durationMs, json, root) {
119
162
  if (json) {
120
- const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0' };
163
+ const out = { items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }], summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } }, version: '1.0', root: toRootOutput(root) };
121
164
  console.log(JSON.stringify(out, null, 2));
122
165
  return;
123
166
  }
@@ -131,15 +174,15 @@ export class ValidateCommand {
131
174
  const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
132
175
  console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
133
176
  }
134
- this.printNextSteps(type);
177
+ this.printNextSteps(type, id, root);
135
178
  }
136
179
  }
137
- printNextSteps(type) {
180
+ printNextSteps(type, id, root) {
138
181
  const bullets = [];
139
182
  if (type === 'change') {
140
183
  bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
141
184
  bullets.push('- Each requirement MUST include at least one #### Scenario: block');
142
- bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
185
+ bullets.push(`- Debug parsed deltas: ${withStoreFlag(root, `openspec show ${id} --json --deltas-only`)}`);
143
186
  }
144
187
  else {
145
188
  bullets.push('- Ensure spec includes ## Purpose and ## Requirements sections');
@@ -149,11 +192,11 @@ export class ValidateCommand {
149
192
  console.error('Next steps:');
150
193
  bullets.forEach(b => console.error(` ${b}`));
151
194
  }
152
- async runBulkValidation(scope, opts) {
195
+ async runBulkValidation(root, scope, opts) {
153
196
  const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
154
197
  const [changeIds, specIds] = await Promise.all([
155
- scope.changes ? getActiveChangeIds() : Promise.resolve([]),
156
- scope.specs ? getSpecIds() : Promise.resolve([]),
198
+ scope.changes ? this.listChangeIds(root) : Promise.resolve([]),
199
+ scope.specs ? getSpecIds(root.path) : Promise.resolve([]),
157
200
  ]);
158
201
  const DEFAULT_CONCURRENCY = 6;
159
202
  const maxSuggestions = 5; // used by nearestMatches
@@ -163,7 +206,7 @@ export class ValidateCommand {
163
206
  for (const id of changeIds) {
164
207
  queue.push(async () => {
165
208
  const start = Date.now();
166
- const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
209
+ const changeDir = path.join(root.changesDir, id);
167
210
  const report = await validator.validateChangeDeltaSpecs(changeDir);
168
211
  const durationMs = Date.now() - start;
169
212
  return { id, type: 'change', valid: report.valid, issues: report.issues, durationMs };
@@ -172,7 +215,7 @@ export class ValidateCommand {
172
215
  for (const id of specIds) {
173
216
  queue.push(async () => {
174
217
  const start = Date.now();
175
- const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
218
+ const file = path.join(root.specsDir, id, 'spec.md');
176
219
  const report = await validator.validateSpec(file);
177
220
  const durationMs = Date.now() - start;
178
221
  return { id, type: 'spec', valid: report.valid, issues: report.issues, durationMs };
@@ -188,7 +231,7 @@ export class ValidateCommand {
188
231
  },
189
232
  };
190
233
  if (opts.json) {
191
- const out = { items: [], summary, version: '1.0' };
234
+ const out = { items: [], summary, version: '1.0', root: toRootOutput(root) };
192
235
  console.log(JSON.stringify(out, null, 2));
193
236
  }
194
237
  else {
@@ -245,7 +288,7 @@ export class ValidateCommand {
245
288
  },
246
289
  };
247
290
  if (opts.json) {
248
- const out = { items: results, summary, version: '1.0' };
291
+ const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
249
292
  console.log(JSON.stringify(out, null, 2));
250
293
  }
251
294
  else {
@@ -256,6 +299,11 @@ export class ValidateCommand {
256
299
  console.error(`✗ ${res.type}/${res.id}`);
257
300
  }
258
301
  console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
302
+ const firstFailure = results.find((res) => !res.valid);
303
+ if (firstFailure) {
304
+ const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
305
+ console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
306
+ }
259
307
  }
260
308
  process.exitCode = failed > 0 ? 1 : 0;
261
309
  }
@@ -13,7 +13,5 @@ export { schemasCommand } from './schemas.js';
13
13
  export type { SchemasOptions } from './schemas.js';
14
14
  export { newChangeCommand } from './new-change.js';
15
15
  export type { NewChangeOptions } from './new-change.js';
16
- export { setChangeCommand } from './set-change.js';
17
- export type { SetChangeOptions } from './set-change.js';
18
16
  export { DEFAULT_SCHEMA } from './shared.js';
19
17
  //# sourceMappingURL=index.d.ts.map
@@ -8,6 +8,5 @@ export { instructionsCommand, applyInstructionsCommand } from './instructions.js
8
8
  export { templatesCommand } from './templates.js';
9
9
  export { schemasCommand } from './schemas.js';
10
10
  export { newChangeCommand } from './new-change.js';
11
- export { setChangeCommand } from './set-change.js';
12
11
  export { DEFAULT_SCHEMA } from './shared.js';
13
12
  //# sourceMappingURL=index.js.map
@@ -5,25 +5,35 @@
5
5
  * Includes both artifact instructions and apply instructions.
6
6
  */
7
7
  import { type ArtifactInstructions } from '../../core/artifact-graph/index.js';
8
+ import { type PlanningHome } from '../../core/planning-home.js';
9
+ import { type ReferenceIndexEntry } from '../../core/references.js';
8
10
  import { type ApplyInstructions } from './shared.js';
9
11
  export interface InstructionsOptions {
10
12
  change?: string;
11
13
  schema?: string;
14
+ store?: string;
15
+ storePath?: string;
12
16
  json?: boolean;
13
17
  }
14
18
  export interface ApplyInstructionsOptions {
15
19
  change?: string;
16
20
  schema?: string;
21
+ store?: string;
22
+ storePath?: string;
17
23
  json?: boolean;
18
24
  }
19
25
  export declare function instructionsCommand(artifactId: string | undefined, options: InstructionsOptions): Promise<void>;
20
26
  export declare function printInstructionsText(instructions: ArtifactInstructions, isBlocked: boolean): void;
27
+ export interface GenerateApplyInstructionsOptions {
28
+ planningHome?: PlanningHome;
29
+ references?: ReferenceIndexEntry[];
30
+ }
21
31
  /**
22
32
  * Generates apply instructions for implementing tasks from a change.
23
33
  * Schema-aware: reads apply phase configuration from schema to determine
24
34
  * required artifacts, tracking file, and instruction.
25
35
  */
26
- export declare function generateApplyInstructions(projectRoot: string, changeName: string, schemaName?: string, planningHome?: import("../../core/planning-home.js").PlanningHome): Promise<ApplyInstructions>;
36
+ export declare function generateApplyInstructions(projectRoot: string, changeName: string, schemaName?: string, options?: GenerateApplyInstructionsOptions): Promise<ApplyInstructions>;
27
37
  export declare function applyInstructionsCommand(options: ApplyInstructionsOptions): Promise<void>;
28
38
  export declare function printApplyInstructionsText(instructions: ApplyInstructions): void;
29
39
  //# sourceMappingURL=instructions.d.ts.map
@@ -8,17 +8,49 @@ import ora from 'ora';
8
8
  import path from 'path';
9
9
  import * as fs from 'fs';
10
10
  import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
11
- import { getChangeDir, resolveCurrentPlanningHomeSync } from '../../core/planning-home.js';
11
+ import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
12
+ import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
13
+ import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
14
+ import { readRegistrySnapshot } from '../../core/store/registry.js';
15
+ import { readProjectConfig } from '../../core/project-config.js';
12
16
  import { validateChangeExists, validateSchemaExists, } from './shared.js';
13
17
  // -----------------------------------------------------------------------------
14
18
  // Artifact Instructions Command
15
19
  // -----------------------------------------------------------------------------
20
+ /**
21
+ * Reads the resolved root's config once, assembles the referenced-store
22
+ * index when references are declared, and resolves the config path for
23
+ * fix text. Shared by both instruction surfaces.
24
+ */
25
+ async function loadRootConfigContext(root) {
26
+ // readProjectConfig never throws: missing/unparseable configs are null.
27
+ const projectConfig = readProjectConfig(root.path);
28
+ // One registry read serves every relationship consumer in this
29
+ // output so it never carries a torn snapshot.
30
+ const snapshot = await readRegistrySnapshot();
31
+ const registryEntries = snapshot.entries;
32
+ const declared = projectConfig?.references ?? [];
33
+ const index = declared.length > 0
34
+ ? await assembleReferenceIndex({ references: declared, resolvedRoot: root, registryEntries })
35
+ : [];
36
+ // Omitted, not empty: an index emptied by self-reference omission must
37
+ // look identical to an undeclared one in JSON.
38
+ return {
39
+ projectConfig,
40
+ references: index.length > 0 ? index : undefined,
41
+ };
42
+ }
16
43
  export async function instructionsCommand(artifactId, options) {
44
+ // Resolve (and banner) before the spinner starts so stderr stays readable.
45
+ const root = await resolveRootForCommand(options, { json: options.json });
46
+ if (!root) {
47
+ return;
48
+ }
17
49
  const spinner = options.json ? undefined : ora('Generating instructions...').start();
18
50
  try {
19
- const planningHome = resolveCurrentPlanningHomeSync();
20
- const projectRoot = planningHome.root;
21
- const changeName = await validateChangeExists(options.change, projectRoot, planningHome.changesDir);
51
+ const planningHome = toPlanningHome(root);
52
+ const projectRoot = root.path;
53
+ const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
22
54
  // Validate schema if explicitly provided
23
55
  if (options.schema) {
24
56
  validateSchemaExists(options.schema, projectRoot);
@@ -39,11 +71,15 @@ export async function instructionsCommand(artifactId, options) {
39
71
  const validIds = context.graph.getAllArtifacts().map((a) => a.id);
40
72
  throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'. Valid artifacts:\n ${validIds.join('\n ')}`);
41
73
  }
42
- const instructions = generateInstructions(context, artifactId, projectRoot);
74
+ const { projectConfig, references } = await loadRootConfigContext(root);
75
+ const instructions = generateInstructions(context, artifactId, projectRoot, {
76
+ projectConfig,
77
+ references,
78
+ });
43
79
  const isBlocked = instructions.dependencies.some((d) => !d.done);
44
80
  spinner?.stop();
45
81
  if (options.json) {
46
- console.log(JSON.stringify(instructions, null, 2));
82
+ console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
47
83
  return;
48
84
  }
49
85
  printInstructionsText(instructions, isBlocked);
@@ -54,14 +90,10 @@ export async function instructionsCommand(artifactId, options) {
54
90
  }
55
91
  }
56
92
  export function printInstructionsText(instructions, isBlocked) {
57
- const { artifactId, changeName, schemaName, changeDir, initiative, resolvedOutputPath, description, instruction, context, rules, template, dependencies, unlocks, } = instructions;
93
+ const { artifactId, changeName, schemaName, changeDir, resolvedOutputPath, description, instruction, context, rules, template, dependencies, unlocks, } = instructions;
58
94
  // Opening tag
59
95
  console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
60
96
  console.log();
61
- if (initiative) {
62
- console.log(`<initiative store="${initiative.store}" id="${initiative.id}" />`);
63
- console.log();
64
- }
65
97
  // Warning for blocked artifacts
66
98
  if (isBlocked) {
67
99
  const missing = dependencies.filter((d) => !d.done).map((d) => d.id);
@@ -85,6 +117,11 @@ export function printInstructionsText(instructions, isBlocked) {
85
117
  console.log('</project_context>');
86
118
  console.log();
87
119
  }
120
+ // Referenced-store index (read-only upstream context)
121
+ if (instructions.references && instructions.references.length > 0) {
122
+ console.log(renderReferencedStoresBlock(instructions.references));
123
+ console.log();
124
+ }
88
125
  // Rules (AI constraint - do not include in output)
89
126
  if (rules && rules.length > 0) {
90
127
  console.log('<rules>');
@@ -175,7 +212,9 @@ function parseTasksFile(content) {
175
212
  * Schema-aware: reads apply phase configuration from schema to determine
176
213
  * required artifacts, tracking file, and instruction.
177
214
  */
178
- export async function generateApplyInstructions(projectRoot, changeName, schemaName, planningHome = resolveCurrentPlanningHomeSync({ startPath: projectRoot })) {
215
+ export async function generateApplyInstructions(projectRoot, changeName, schemaName, options = {}) {
216
+ const planningHome = options.planningHome ?? resolveCurrentPlanningHomeSync({ startPath: projectRoot });
217
+ const references = options.references;
179
218
  // loadChangeContext will auto-detect schema from metadata if not provided
180
219
  const context = loadChangeContext(projectRoot, changeName, schemaName, {
181
220
  changeDir: getChangeDir(planningHome, changeName),
@@ -257,30 +296,39 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
257
296
  changeName,
258
297
  changeDir,
259
298
  schemaName: context.schemaName,
260
- ...(context.initiative ? { initiative: context.initiative } : {}),
261
299
  contextFiles,
262
300
  progress: { total, complete, remaining },
263
301
  tasks,
264
302
  state,
265
303
  missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
266
304
  instruction,
305
+ ...(references !== undefined ? { references } : {}),
267
306
  };
268
307
  }
269
308
  export async function applyInstructionsCommand(options) {
309
+ // Resolve (and banner) before the spinner starts so stderr stays readable.
310
+ const root = await resolveRootForCommand(options, { json: options.json });
311
+ if (!root) {
312
+ return;
313
+ }
270
314
  const spinner = options.json ? undefined : ora('Generating apply instructions...').start();
271
315
  try {
272
- const planningHome = resolveCurrentPlanningHomeSync();
273
- const projectRoot = planningHome.root;
274
- const changeName = await validateChangeExists(options.change, projectRoot, planningHome.changesDir);
316
+ const planningHome = toPlanningHome(root);
317
+ const projectRoot = root.path;
318
+ const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: withStoreFlag(root, 'openspec new change <name>') });
275
319
  // Validate schema if explicitly provided
276
320
  if (options.schema) {
277
321
  validateSchemaExists(options.schema, projectRoot);
278
322
  }
279
323
  // generateApplyInstructions uses loadChangeContext which auto-detects schema
280
- const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, planningHome);
324
+ const { references } = await loadRootConfigContext(root);
325
+ const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
326
+ planningHome,
327
+ references,
328
+ });
281
329
  spinner?.stop();
282
330
  if (options.json) {
283
- console.log(JSON.stringify(instructions, null, 2));
331
+ console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
284
332
  return;
285
333
  }
286
334
  printApplyInstructionsText(instructions);
@@ -291,13 +339,14 @@ export async function applyInstructionsCommand(options) {
291
339
  }
292
340
  }
293
341
  export function printApplyInstructionsText(instructions) {
294
- const { changeName, schemaName, initiative, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
342
+ const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
295
343
  console.log(`## Apply: ${changeName}`);
296
344
  console.log(`Schema: ${schemaName}`);
297
- if (initiative) {
298
- console.log(`Initiative: ${initiative.store}/${initiative.id}`);
299
- }
300
345
  console.log();
346
+ if (instructions.references && instructions.references.length > 0) {
347
+ console.log(renderReferencedStoresSection(instructions.references));
348
+ console.log();
349
+ }
301
350
  // Warning for blocked state
302
351
  if (state === 'blocked' && missingArtifacts) {
303
352
  console.log('### ⚠️ Blocked');
@@ -1,16 +1,19 @@
1
1
  /**
2
2
  * New Change Command
3
3
  *
4
- * Creates a new change directory with optional description and schema.
4
+ * Creates a new change directory with optional description and schema in the
5
+ * resolved OpenSpec root. `--store <id>` selects a registered store's
6
+ * root; initiative linking and workspace affected areas are no longer part of
7
+ * this command.
5
8
  */
6
9
  export interface NewChangeOptions {
7
10
  description?: string;
8
11
  goal?: string;
9
- areas?: string;
10
12
  schema?: string;
11
- initiative?: string;
12
13
  store?: string;
13
14
  storePath?: string;
15
+ initiative?: string;
16
+ areas?: string;
14
17
  json?: boolean;
15
18
  }
16
19
  export declare function newChangeCommand(name: string | undefined, options: NewChangeOptions): Promise<void>;