@fission-ai/openspec 1.4.1 → 1.5.0

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 (206) hide show
  1. package/README.md +24 -3
  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 -1
  5. package/dist/commands/change.js +17 -15
  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 +2 -0
  22. package/dist/commands/validate.js +75 -39
  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 +209 -56
  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.js +1 -14
  50. package/dist/core/command-generation/adapters/cursor.js +1 -14
  51. package/dist/core/command-generation/adapters/pi.js +1 -14
  52. package/dist/core/command-generation/adapters/windsurf.js +1 -14
  53. package/dist/core/command-generation/yaml.d.ts +22 -0
  54. package/dist/core/command-generation/yaml.js +38 -0
  55. package/dist/core/completions/command-registry.js +87 -289
  56. package/dist/core/completions/generators/zsh-generator.js +3 -1
  57. package/dist/core/completions/installers/bash-installer.js +3 -0
  58. package/dist/core/completions/installers/fish-installer.js +8 -0
  59. package/dist/core/completions/installers/powershell-installer.js +30 -2
  60. package/dist/core/completions/installers/zsh-installer.js +3 -0
  61. package/dist/core/completions/shared-flags.d.ts +1 -0
  62. package/dist/core/completions/shared-flags.js +5 -0
  63. package/dist/core/config-schema.d.ts +2 -1
  64. package/dist/core/config-schema.js +26 -0
  65. package/dist/core/file-state.d.ts +36 -0
  66. package/dist/core/file-state.js +112 -0
  67. package/dist/core/global-config.d.ts +2 -0
  68. package/dist/core/id.d.ts +17 -0
  69. package/dist/core/id.js +30 -0
  70. package/dist/core/index.d.ts +3 -3
  71. package/dist/core/index.js +3 -3
  72. package/dist/core/init.js +24 -4
  73. package/dist/core/list.d.ts +2 -0
  74. package/dist/core/list.js +19 -5
  75. package/dist/core/openers.d.ts +77 -0
  76. package/dist/core/openers.js +251 -0
  77. package/dist/core/openspec-root.d.ts +45 -0
  78. package/dist/core/openspec-root.js +192 -0
  79. package/dist/core/planning-home.d.ts +1 -6
  80. package/dist/core/planning-home.js +2 -43
  81. package/dist/core/project-config.d.ts +44 -1
  82. package/dist/core/project-config.js +160 -10
  83. package/dist/core/references.d.ts +63 -0
  84. package/dist/core/references.js +310 -0
  85. package/dist/core/relationship-health.d.ts +65 -0
  86. package/dist/core/relationship-health.js +64 -0
  87. package/dist/core/root-selection.d.ts +122 -0
  88. package/dist/core/root-selection.js +337 -0
  89. package/dist/core/specs-apply.d.ts +6 -1
  90. package/dist/core/specs-apply.js +6 -4
  91. package/dist/core/store/errors.d.ts +20 -0
  92. package/dist/core/{context-store → store}/errors.js +3 -3
  93. package/dist/core/store/foundation.d.ts +56 -0
  94. package/dist/core/store/foundation.js +251 -0
  95. package/dist/core/store/git.d.ts +23 -0
  96. package/dist/core/store/git.js +137 -0
  97. package/dist/core/{context-store → store}/index.d.ts +0 -1
  98. package/dist/core/{context-store → store}/index.js +0 -1
  99. package/dist/core/store/operations.d.ts +114 -0
  100. package/dist/core/store/operations.js +783 -0
  101. package/dist/core/store/registry.d.ts +58 -0
  102. package/dist/core/store/registry.js +275 -0
  103. package/dist/core/templates/workflows/apply-change.js +5 -4
  104. package/dist/core/templates/workflows/archive-change.js +5 -4
  105. package/dist/core/templates/workflows/bulk-archive-change.js +5 -4
  106. package/dist/core/templates/workflows/continue-change.js +7 -2
  107. package/dist/core/templates/workflows/explore.js +5 -0
  108. package/dist/core/templates/workflows/ff-change.js +5 -0
  109. package/dist/core/templates/workflows/new-change.js +5 -0
  110. package/dist/core/templates/workflows/onboard.js +4 -1
  111. package/dist/core/templates/workflows/propose.js +5 -0
  112. package/dist/core/templates/workflows/store-selection.d.ts +8 -0
  113. package/dist/core/templates/workflows/store-selection.js +8 -0
  114. package/dist/core/templates/workflows/sync-specs.js +5 -4
  115. package/dist/core/templates/workflows/verify-change.js +5 -4
  116. package/dist/core/working-set.d.ts +47 -0
  117. package/dist/core/working-set.js +43 -0
  118. package/dist/core/worksets.d.ts +75 -0
  119. package/dist/core/worksets.js +245 -0
  120. package/dist/core/zod-issues.d.ts +4 -0
  121. package/dist/core/zod-issues.js +10 -0
  122. package/dist/utils/change-metadata.d.ts +1 -0
  123. package/dist/utils/change-metadata.js +1 -1
  124. package/dist/utils/change-utils.js +15 -0
  125. package/dist/utils/file-system.js +36 -17
  126. package/package.json +6 -6
  127. package/dist/commands/context-store.d.ts +0 -3
  128. package/dist/commands/context-store.js +0 -475
  129. package/dist/commands/initiative.d.ts +0 -13
  130. package/dist/commands/initiative.js +0 -318
  131. package/dist/commands/workflow/initiative-link.d.ts +0 -24
  132. package/dist/commands/workflow/initiative-link.js +0 -47
  133. package/dist/commands/workflow/set-change.d.ts +0 -13
  134. package/dist/commands/workflow/set-change.js +0 -87
  135. package/dist/commands/workspace/context-status.d.ts +0 -4
  136. package/dist/commands/workspace/context-status.js +0 -59
  137. package/dist/commands/workspace/open-target-selection.d.ts +0 -13
  138. package/dist/commands/workspace/open-target-selection.js +0 -146
  139. package/dist/commands/workspace/open-view.d.ts +0 -62
  140. package/dist/commands/workspace/open-view.js +0 -249
  141. package/dist/commands/workspace/open.d.ts +0 -37
  142. package/dist/commands/workspace/open.js +0 -110
  143. package/dist/commands/workspace/opener-selection.d.ts +0 -11
  144. package/dist/commands/workspace/opener-selection.js +0 -98
  145. package/dist/commands/workspace/operations.d.ts +0 -29
  146. package/dist/commands/workspace/operations.js +0 -543
  147. package/dist/commands/workspace/prompt-theme.d.ts +0 -29
  148. package/dist/commands/workspace/prompt-theme.js +0 -24
  149. package/dist/commands/workspace/registration.d.ts +0 -13
  150. package/dist/commands/workspace/registration.js +0 -84
  151. package/dist/commands/workspace/selection.d.ts +0 -8
  152. package/dist/commands/workspace/selection.js +0 -104
  153. package/dist/commands/workspace/setup-prompts.d.ts +0 -13
  154. package/dist/commands/workspace/setup-prompts.js +0 -121
  155. package/dist/commands/workspace/types.d.ts +0 -103
  156. package/dist/commands/workspace/types.js +0 -36
  157. package/dist/commands/workspace.d.ts +0 -5
  158. package/dist/commands/workspace.js +0 -574
  159. package/dist/core/collections/index.d.ts +0 -3
  160. package/dist/core/collections/index.js +0 -3
  161. package/dist/core/collections/initiatives/collection.d.ts +0 -4
  162. package/dist/core/collections/initiatives/collection.js +0 -17
  163. package/dist/core/collections/initiatives/index.d.ts +0 -6
  164. package/dist/core/collections/initiatives/index.js +0 -6
  165. package/dist/core/collections/initiatives/operations.d.ts +0 -49
  166. package/dist/core/collections/initiatives/operations.js +0 -175
  167. package/dist/core/collections/initiatives/resolution.d.ts +0 -87
  168. package/dist/core/collections/initiatives/resolution.js +0 -374
  169. package/dist/core/collections/initiatives/schema.d.ts +0 -41
  170. package/dist/core/collections/initiatives/schema.js +0 -134
  171. package/dist/core/collections/initiatives/templates.d.ts +0 -12
  172. package/dist/core/collections/initiatives/templates.js +0 -90
  173. package/dist/core/collections/runtime.d.ts +0 -46
  174. package/dist/core/collections/runtime.js +0 -194
  175. package/dist/core/context-store/binding.d.ts +0 -53
  176. package/dist/core/context-store/binding.js +0 -197
  177. package/dist/core/context-store/errors.d.ts +0 -20
  178. package/dist/core/context-store/foundation.d.ts +0 -55
  179. package/dist/core/context-store/foundation.js +0 -321
  180. package/dist/core/context-store/operations.d.ts +0 -85
  181. package/dist/core/context-store/operations.js +0 -528
  182. package/dist/core/context-store/registry.d.ts +0 -45
  183. package/dist/core/context-store/registry.js +0 -229
  184. package/dist/core/workspace/foundation.d.ts +0 -67
  185. package/dist/core/workspace/foundation.js +0 -295
  186. package/dist/core/workspace/index.d.ts +0 -8
  187. package/dist/core/workspace/index.js +0 -8
  188. package/dist/core/workspace/legacy-state.d.ts +0 -28
  189. package/dist/core/workspace/legacy-state.js +0 -200
  190. package/dist/core/workspace/link-input.d.ts +0 -9
  191. package/dist/core/workspace/link-input.js +0 -32
  192. package/dist/core/workspace/open-surface.d.ts +0 -45
  193. package/dist/core/workspace/open-surface.js +0 -215
  194. package/dist/core/workspace/openers.d.ts +0 -21
  195. package/dist/core/workspace/openers.js +0 -124
  196. package/dist/core/workspace/registry.d.ts +0 -24
  197. package/dist/core/workspace/registry.js +0 -146
  198. package/dist/core/workspace/skills.d.ts +0 -57
  199. package/dist/core/workspace/skills.js +0 -334
  200. package/dist/core/workspace/state-io.d.ts +0 -10
  201. package/dist/core/workspace/state-io.js +0 -121
  202. package/schemas/workspace-planning/schema.yaml +0 -72
  203. package/schemas/workspace-planning/templates/design.md +0 -33
  204. package/schemas/workspace-planning/templates/proposal.md +0 -28
  205. package/schemas/workspace-planning/templates/spec.md +0 -9
  206. package/schemas/workspace-planning/templates/tasks.md +0 -15
@@ -1,15 +1,20 @@
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
6
  import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
6
7
  import { nearestMatches } from '../utils/match.js';
7
8
  export class ValidateCommand {
8
9
  async execute(itemName, options = {}) {
10
+ const root = await resolveRootForCommand(options, { json: options.json });
11
+ if (!root) {
12
+ return;
13
+ }
9
14
  const interactive = isInteractive(options);
10
15
  // Handle bulk flags first
11
16
  if (options.all || options.changes || options.specs) {
12
- await this.runBulkValidation({
17
+ await this.runBulkValidation(root, {
13
18
  changes: !!options.all || !!options.changes,
14
19
  specs: !!options.all || !!options.specs,
15
20
  }, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
@@ -18,16 +23,16 @@ export class ValidateCommand {
18
23
  // No item and no flags
19
24
  if (!itemName) {
20
25
  if (interactive) {
21
- await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
26
+ await this.runInteractiveSelector(root, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
22
27
  return;
23
28
  }
24
- this.printNonInteractiveHint();
29
+ this.printNonInteractiveHint(root);
25
30
  process.exitCode = 1;
26
31
  return;
27
32
  }
28
33
  // Direct item validation with type detection or override
29
34
  const typeOverride = this.normalizeType(options.type);
30
- await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
35
+ await this.validateDirectItem(root, itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
31
36
  }
32
37
  normalizeType(value) {
33
38
  if (!value)
@@ -37,7 +42,7 @@ export class ValidateCommand {
37
42
  return v;
38
43
  return undefined;
39
44
  }
40
- async runInteractiveSelector(opts) {
45
+ async runInteractiveSelector(root, opts) {
41
46
  const { select } = await import('@inquirer/prompts');
42
47
  const choice = await select({
43
48
  message: 'What would you like to validate?',
@@ -49,13 +54,13 @@ export class ValidateCommand {
49
54
  ],
50
55
  });
51
56
  if (choice === 'all')
52
- return this.runBulkValidation({ changes: true, specs: true }, opts);
57
+ return this.runBulkValidation(root, { changes: true, specs: true }, opts);
53
58
  if (choice === 'changes')
54
- return this.runBulkValidation({ changes: true, specs: false }, opts);
59
+ return this.runBulkValidation(root, { changes: true, specs: false }, opts);
55
60
  if (choice === 'specs')
56
- return this.runBulkValidation({ changes: false, specs: true }, opts);
61
+ return this.runBulkValidation(root, { changes: false, specs: true }, opts);
57
62
  // one
58
- const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
63
+ const [changes, specs] = await Promise.all([getActiveChangeIds(root.path), getSpecIds(root.path)]);
59
64
  const items = [];
60
65
  items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
61
66
  items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec', id } })));
@@ -65,59 +70,85 @@ export class ValidateCommand {
65
70
  return;
66
71
  }
67
72
  const picked = await select({ message: 'Pick an item', choices: items });
68
- await this.validateByType(picked.type, picked.id, opts);
73
+ await this.validateByType(root, picked.type, picked.id, opts);
69
74
  }
70
- printNonInteractiveHint() {
75
+ printNonInteractiveHint(root) {
71
76
  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>');
77
+ console.error(` ${withStoreFlag(root, 'openspec validate --all')}`);
78
+ console.error(` ${withStoreFlag(root, 'openspec validate --changes')}`);
79
+ console.error(` ${withStoreFlag(root, 'openspec validate --specs')}`);
80
+ console.error(` ${withStoreFlag(root, 'openspec validate <item-name>')}`);
76
81
  console.error('Or run in an interactive terminal.');
77
82
  }
78
- async validateDirectItem(itemName, opts) {
79
- const [changes, specs] = await Promise.all([getActiveChangeIds(), getSpecIds()]);
83
+ async validateDirectItem(root, itemName, opts) {
84
+ const [changes, specs] = await Promise.all([getActiveChangeIds(root.path), getSpecIds(root.path)]);
80
85
  const isChange = changes.includes(itemName);
81
86
  const isSpec = specs.includes(itemName);
82
87
  const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
83
88
  if (!type) {
84
- console.error(`Unknown item '${itemName}'`);
85
89
  const suggestions = nearestMatches(itemName, [...changes, ...specs]);
86
- if (suggestions.length)
87
- console.error(`Did you mean: ${suggestions.join(', ')}?`);
90
+ const message = suggestions.length
91
+ ? `Unknown item '${itemName}'. Did you mean: ${suggestions.join(', ')}?`
92
+ : `Unknown item '${itemName}'.`;
93
+ if (opts.json) {
94
+ console.log(JSON.stringify({ status: [{ severity: 'error', code: 'unknown_item', message }] }, null, 2));
95
+ }
96
+ else {
97
+ console.error(message);
98
+ }
88
99
  process.exitCode = 1;
89
100
  return;
90
101
  }
91
102
  if (!opts.typeOverride && isChange && isSpec) {
103
+ if (opts.json) {
104
+ console.log(JSON.stringify({
105
+ status: [
106
+ {
107
+ severity: 'error',
108
+ code: 'ambiguous_item',
109
+ message: `Ambiguous item '${itemName}' matches both a change and a spec.`,
110
+ fix: 'Pass --type change|spec.',
111
+ },
112
+ ],
113
+ }, null, 2));
114
+ process.exitCode = 1;
115
+ return;
116
+ }
92
117
  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');
118
+ // The noun-form commands are cwd-based and cannot reach a selected store.
119
+ if (isStoreSelectedRoot(root)) {
120
+ console.error('Pass --type change|spec.');
121
+ }
122
+ else {
123
+ console.error('Pass --type change|spec, or use: openspec change validate / openspec spec validate');
124
+ }
94
125
  process.exitCode = 1;
95
126
  return;
96
127
  }
97
- await this.validateByType(type, itemName, opts);
128
+ await this.validateByType(root, type, itemName, opts);
98
129
  }
99
- async validateByType(type, id, opts) {
130
+ async validateByType(root, type, id, opts) {
100
131
  const validator = new Validator(opts.strict);
101
132
  if (type === 'change') {
102
- const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
133
+ const changeDir = path.join(root.changesDir, id);
103
134
  const start = Date.now();
104
135
  const report = await validator.validateChangeDeltaSpecs(changeDir);
105
136
  const durationMs = Date.now() - start;
106
- this.printReport('change', id, report, durationMs, opts.json);
137
+ this.printReport('change', id, report, durationMs, opts.json, root);
107
138
  // Non-zero exit if invalid (keeps enriched output test semantics)
108
139
  process.exitCode = report.valid ? 0 : 1;
109
140
  return;
110
141
  }
111
- const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
142
+ const file = path.join(root.specsDir, id, 'spec.md');
112
143
  const start = Date.now();
113
144
  const report = await validator.validateSpec(file);
114
145
  const durationMs = Date.now() - start;
115
- this.printReport('spec', id, report, durationMs, opts.json);
146
+ this.printReport('spec', id, report, durationMs, opts.json, root);
116
147
  process.exitCode = report.valid ? 0 : 1;
117
148
  }
118
- printReport(type, id, report, durationMs, json) {
149
+ printReport(type, id, report, durationMs, json, root) {
119
150
  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' };
151
+ 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
152
  console.log(JSON.stringify(out, null, 2));
122
153
  return;
123
154
  }
@@ -131,15 +162,15 @@ export class ValidateCommand {
131
162
  const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
132
163
  console.error(`${prefix} [${label}] ${issue.path}: ${issue.message}`);
133
164
  }
134
- this.printNextSteps(type);
165
+ this.printNextSteps(type, id, root);
135
166
  }
136
167
  }
137
- printNextSteps(type) {
168
+ printNextSteps(type, id, root) {
138
169
  const bullets = [];
139
170
  if (type === 'change') {
140
171
  bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
141
172
  bullets.push('- Each requirement MUST include at least one #### Scenario: block');
142
- bullets.push('- Debug parsed deltas: openspec change show <id> --json --deltas-only');
173
+ bullets.push(`- Debug parsed deltas: ${withStoreFlag(root, `openspec show ${id} --json --deltas-only`)}`);
143
174
  }
144
175
  else {
145
176
  bullets.push('- Ensure spec includes ## Purpose and ## Requirements sections');
@@ -149,11 +180,11 @@ export class ValidateCommand {
149
180
  console.error('Next steps:');
150
181
  bullets.forEach(b => console.error(` ${b}`));
151
182
  }
152
- async runBulkValidation(scope, opts) {
183
+ async runBulkValidation(root, scope, opts) {
153
184
  const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
154
185
  const [changeIds, specIds] = await Promise.all([
155
- scope.changes ? getActiveChangeIds() : Promise.resolve([]),
156
- scope.specs ? getSpecIds() : Promise.resolve([]),
186
+ scope.changes ? getActiveChangeIds(root.path) : Promise.resolve([]),
187
+ scope.specs ? getSpecIds(root.path) : Promise.resolve([]),
157
188
  ]);
158
189
  const DEFAULT_CONCURRENCY = 6;
159
190
  const maxSuggestions = 5; // used by nearestMatches
@@ -163,7 +194,7 @@ export class ValidateCommand {
163
194
  for (const id of changeIds) {
164
195
  queue.push(async () => {
165
196
  const start = Date.now();
166
- const changeDir = path.join(process.cwd(), 'openspec', 'changes', id);
197
+ const changeDir = path.join(root.changesDir, id);
167
198
  const report = await validator.validateChangeDeltaSpecs(changeDir);
168
199
  const durationMs = Date.now() - start;
169
200
  return { id, type: 'change', valid: report.valid, issues: report.issues, durationMs };
@@ -172,7 +203,7 @@ export class ValidateCommand {
172
203
  for (const id of specIds) {
173
204
  queue.push(async () => {
174
205
  const start = Date.now();
175
- const file = path.join(process.cwd(), 'openspec', 'specs', id, 'spec.md');
206
+ const file = path.join(root.specsDir, id, 'spec.md');
176
207
  const report = await validator.validateSpec(file);
177
208
  const durationMs = Date.now() - start;
178
209
  return { id, type: 'spec', valid: report.valid, issues: report.issues, durationMs };
@@ -188,7 +219,7 @@ export class ValidateCommand {
188
219
  },
189
220
  };
190
221
  if (opts.json) {
191
- const out = { items: [], summary, version: '1.0' };
222
+ const out = { items: [], summary, version: '1.0', root: toRootOutput(root) };
192
223
  console.log(JSON.stringify(out, null, 2));
193
224
  }
194
225
  else {
@@ -245,7 +276,7 @@ export class ValidateCommand {
245
276
  },
246
277
  };
247
278
  if (opts.json) {
248
- const out = { items: results, summary, version: '1.0' };
279
+ const out = { items: results, summary, version: '1.0', root: toRootOutput(root) };
249
280
  console.log(JSON.stringify(out, null, 2));
250
281
  }
251
282
  else {
@@ -256,6 +287,11 @@ export class ValidateCommand {
256
287
  console.error(`✗ ${res.type}/${res.id}`);
257
288
  }
258
289
  console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
290
+ const firstFailure = results.find((res) => !res.valid);
291
+ if (firstFailure) {
292
+ const storeFlag = isStoreSelectedRoot(root) ? ` --store ${root.storeId}` : '';
293
+ console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
294
+ }
259
295
  }
260
296
  process.exitCode = failed > 0 ? 1 : 0;
261
297
  }
@@ -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>;