@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
@@ -3,7 +3,51 @@ import path from 'path';
3
3
  import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
4
4
  import { Validator } from './validation/validator.js';
5
5
  import chalk from 'chalk';
6
+ import { emitStoreRootBanner, isRootSelectionError, resolveOpenSpecRoot, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from './root-selection.js';
6
7
  import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
8
+ async function listActiveChangeNames(changesDir) {
9
+ try {
10
+ const entries = await fs.readdir(changesDir, { withFileTypes: true });
11
+ return entries
12
+ .filter((entry) => entry.isDirectory() && entry.name !== 'archive')
13
+ .map((entry) => entry.name)
14
+ .sort();
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ }
20
+ /**
21
+ * JSON mode is non-interactive: any point where the human flow would prompt or
22
+ * print prose instead throws this error, which becomes a machine-readable
23
+ * status entry with a non-zero exit code.
24
+ */
25
+ class ArchiveBlockedError extends Error {
26
+ diagnostic;
27
+ constructor(code, message, fix) {
28
+ super(message);
29
+ this.name = 'ArchiveBlockedError';
30
+ this.diagnostic = {
31
+ severity: 'error',
32
+ code,
33
+ message,
34
+ ...(fix ? { fix } : {}),
35
+ };
36
+ }
37
+ }
38
+ function toArchiveDiagnostic(error) {
39
+ if (error instanceof ArchiveBlockedError) {
40
+ return error.diagnostic;
41
+ }
42
+ if (isRootSelectionError(error)) {
43
+ return error.diagnostic;
44
+ }
45
+ return {
46
+ severity: 'error',
47
+ code: 'archive_error',
48
+ message: error instanceof Error ? error.message : String(error),
49
+ };
50
+ }
7
51
  /**
8
52
  * Recursively copy a directory. Used when fs.rename fails (e.g. EPERM on Windows).
9
53
  */
@@ -44,10 +88,54 @@ async function moveDirectory(src, dest) {
44
88
  }
45
89
  export class ArchiveCommand {
46
90
  async execute(changeName, options = {}) {
47
- const targetPath = '.';
48
- const changesDir = path.join(targetPath, 'openspec', 'changes');
49
- const archiveDir = path.join(changesDir, 'archive');
50
- const mainSpecsDir = path.join(targetPath, 'openspec', 'specs');
91
+ const json = !!options.json;
92
+ let root;
93
+ try {
94
+ root = await resolveOpenSpecRoot({
95
+ ...(options.store !== undefined ? { store: options.store } : {}),
96
+ ...(options.storePath !== undefined ? { storePath: options.storePath } : {}),
97
+ });
98
+ }
99
+ catch (error) {
100
+ if (json && isRootSelectionError(error)) {
101
+ this.printJsonFailure(undefined, toArchiveDiagnostic(error));
102
+ return;
103
+ }
104
+ throw error;
105
+ }
106
+ if (json) {
107
+ try {
108
+ const result = await this.run(changeName, options, root, true);
109
+ if (!result) {
110
+ return;
111
+ }
112
+ console.log(JSON.stringify({ archive: result, root: toRootOutput(root) }, null, 2));
113
+ }
114
+ catch (error) {
115
+ this.printJsonFailure(root, toArchiveDiagnostic(error));
116
+ }
117
+ return;
118
+ }
119
+ emitStoreRootBanner(root);
120
+ await this.run(changeName, options, root, false);
121
+ }
122
+ printJsonFailure(root, diagnostic) {
123
+ console.log(JSON.stringify({
124
+ archive: null,
125
+ ...(root ? { root: toRootOutput(root) } : {}),
126
+ status: [diagnostic],
127
+ }, null, 2));
128
+ process.exitCode = 1;
129
+ }
130
+ /**
131
+ * Shared archive flow. In human mode (json=false) prompts and prose match
132
+ * the historical behavior and cancellations return null. In JSON mode no
133
+ * prose reaches stdout and every blocked path throws.
134
+ */
135
+ async run(changeName, options, root, json) {
136
+ const changesDir = root.changesDir;
137
+ const archiveDir = root.archiveDir;
138
+ const mainSpecsDir = root.specsDir;
51
139
  // Check if changes directory exists
52
140
  try {
53
141
  await fs.access(changesDir);
@@ -57,10 +145,13 @@ export class ArchiveCommand {
57
145
  }
58
146
  // Get change name interactively if not provided
59
147
  if (!changeName) {
148
+ if (json) {
149
+ throw new ArchiveBlockedError('archive_change_name_required', 'A change name is required: archive --json is non-interactive.', withStoreFlag(root, 'openspec archive <change-name> --json'));
150
+ }
60
151
  const selectedChange = await this.selectChange(changesDir);
61
152
  if (!selectedChange) {
62
153
  console.log('No change selected. Aborting.');
63
- return;
154
+ return null;
64
155
  }
65
156
  changeName = selectedChange;
66
157
  }
@@ -73,29 +164,34 @@ export class ArchiveCommand {
73
164
  }
74
165
  }
75
166
  catch {
76
- throw new Error(`Change '${changeName}' not found.`);
167
+ const available = await listActiveChangeNames(changesDir);
168
+ throw new ArchiveBlockedError('archive_change_not_found', available.length > 0
169
+ ? `Change '${changeName}' not found. Available changes: ${available.join(', ')}`
170
+ : `Change '${changeName}' not found. No active changes exist in this root.`);
77
171
  }
78
172
  const skipValidation = options.validate === false || options.noValidate === true;
79
173
  // Validate specs and change before archiving
80
174
  if (!skipValidation) {
81
175
  const validator = new Validator();
82
176
  let hasValidationErrors = false;
83
- // Validate proposal.md (non-blocking unless strict mode desired in future)
84
- const changeFile = path.join(changeDir, 'proposal.md');
85
- try {
86
- await fs.access(changeFile);
87
- const changeReport = await validator.validateChange(changeFile);
88
- // Proposal validation is informative only (do not block archive)
89
- if (!changeReport.valid) {
90
- console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
91
- for (const issue of changeReport.issues) {
92
- const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
93
- console.log(chalk.yellow(` ${symbol} ${issue.message}`));
177
+ // Validate proposal.md (informative only; human mode prints warnings)
178
+ if (!json) {
179
+ const changeFile = path.join(changeDir, 'proposal.md');
180
+ try {
181
+ await fs.access(changeFile);
182
+ const changeReport = await validator.validateChange(changeFile);
183
+ // Proposal validation is informative only (do not block archive)
184
+ if (!changeReport.valid) {
185
+ console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
186
+ for (const issue of changeReport.issues) {
187
+ const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
188
+ console.log(chalk.yellow(` ${symbol} ${issue.message}`));
189
+ }
94
190
  }
95
191
  }
96
- }
97
- catch {
98
- // Change file doesn't exist, skip validation
192
+ catch {
193
+ // Change file doesn't exist, skip validation
194
+ }
99
195
  }
100
196
  // Validate delta-formatted spec files under the change directory if present
101
197
  const changeSpecsDir = path.join(changeDir, 'specs');
@@ -122,21 +218,31 @@ export class ArchiveCommand {
122
218
  const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
123
219
  if (!deltaReport.valid) {
124
220
  hasValidationErrors = true;
125
- console.log(chalk.red(`\nValidation errors in change delta specs:`));
126
- for (const issue of deltaReport.issues) {
127
- if (issue.level === 'ERROR') {
128
- console.log(chalk.red(` ✗ ${issue.message}`));
129
- }
130
- else if (issue.level === 'WARNING') {
131
- console.log(chalk.yellow(` ⚠ ${issue.message}`));
221
+ if (!json) {
222
+ console.log(chalk.red(`\nValidation errors in change delta specs:`));
223
+ for (const issue of deltaReport.issues) {
224
+ if (issue.level === 'ERROR') {
225
+ console.log(chalk.red(` ✗ ${issue.message}`));
226
+ }
227
+ else if (issue.level === 'WARNING') {
228
+ console.log(chalk.yellow(` ⚠ ${issue.message}`));
229
+ }
132
230
  }
133
231
  }
134
232
  }
135
233
  }
136
234
  if (hasValidationErrors) {
235
+ if (json) {
236
+ throw new ArchiveBlockedError('archive_validation_failed', `Validation failed for change '${changeName}'.`, `Run ${withStoreFlag(root, `openspec validate ${changeName}`)} for details, fix the errors, or rerun with --no-validate.`);
237
+ }
137
238
  console.log(chalk.red('\nValidation failed. Please fix the errors before archiving.'));
138
239
  console.log(chalk.yellow('To skip validation (not recommended), use --no-validate flag.'));
139
- return;
240
+ return null;
241
+ }
242
+ }
243
+ else if (json) {
244
+ if (!options.yes) {
245
+ throw new ArchiveBlockedError('archive_confirmation_required', 'Skipping validation requires confirmation: rerun with --yes.', withStoreFlag(root, 'openspec archive <change-name> --json --no-validate --yes'));
140
246
  }
141
247
  }
142
248
  else {
@@ -150,7 +256,7 @@ export class ArchiveCommand {
150
256
  });
151
257
  if (!proceed) {
152
258
  console.log('Archive cancelled.');
153
- return;
259
+ return null;
154
260
  }
155
261
  }
156
262
  else {
@@ -161,11 +267,18 @@ export class ArchiveCommand {
161
267
  }
162
268
  // Show progress and check for incomplete tasks
163
269
  const progress = await getTaskProgressForChange(changesDir, changeName);
164
- const status = formatTaskStatus(progress);
165
- console.log(`Task status: ${status}`);
270
+ if (!json) {
271
+ const status = formatTaskStatus(progress);
272
+ console.log(`Task status: ${status}`);
273
+ }
166
274
  const incompleteTasks = Math.max(progress.total - progress.completed, 0);
167
275
  if (incompleteTasks > 0) {
168
- if (!options.yes) {
276
+ if (json) {
277
+ if (!options.yes) {
278
+ throw new ArchiveBlockedError('archive_tasks_incomplete', `${incompleteTasks} incomplete task(s) found for change '${changeName}'.`, 'Complete the tasks or rerun with --yes.');
279
+ }
280
+ }
281
+ else if (!options.yes) {
169
282
  const { confirm } = await import('@inquirer/prompts');
170
283
  const proceed = await confirm({
171
284
  message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
@@ -173,7 +286,7 @@ export class ArchiveCommand {
173
286
  });
174
287
  if (!proceed) {
175
288
  console.log('Archive cancelled.');
176
- return;
289
+ return null;
177
290
  }
178
291
  }
179
292
  else {
@@ -181,21 +294,30 @@ export class ArchiveCommand {
181
294
  }
182
295
  }
183
296
  // Handle spec updates unless skipSpecs flag is set
297
+ let specsUpdated = false;
298
+ let totals;
184
299
  if (options.skipSpecs) {
185
- console.log('Skipping spec updates (--skip-specs flag provided).');
300
+ if (!json) {
301
+ console.log('Skipping spec updates (--skip-specs flag provided).');
302
+ }
186
303
  }
187
304
  else {
188
305
  // Find specs to update
189
306
  const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
190
307
  if (specUpdates.length > 0) {
191
- console.log('\nSpecs to update:');
192
- for (const update of specUpdates) {
193
- const status = update.exists ? 'update' : 'create';
194
- const capability = path.basename(path.dirname(update.target));
195
- console.log(` ${capability}: ${status}`);
308
+ if (!json) {
309
+ console.log('\nSpecs to update:');
310
+ for (const update of specUpdates) {
311
+ const status = update.exists ? 'update' : 'create';
312
+ const capability = path.basename(path.dirname(update.target));
313
+ console.log(` ${capability}: ${status}`);
314
+ }
196
315
  }
197
316
  let shouldUpdateSpecs = true;
198
317
  if (!options.yes) {
318
+ if (json) {
319
+ throw new ArchiveBlockedError('archive_confirmation_required', `Updating ${specUpdates.length} spec(s) requires confirmation: rerun with --yes.`, withStoreFlag(root, 'openspec archive <change-name> --json --yes'));
320
+ }
199
321
  const { confirm } = await import('@inquirer/prompts');
200
322
  shouldUpdateSpecs = await confirm({
201
323
  message: 'Proceed with spec updates?',
@@ -210,22 +332,28 @@ export class ArchiveCommand {
210
332
  const prepared = [];
211
333
  try {
212
334
  for (const update of specUpdates) {
213
- const built = await buildUpdatedSpec(update, changeName);
335
+ const built = await buildUpdatedSpec(update, changeName, { silent: json });
214
336
  prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
215
337
  }
216
338
  }
217
339
  catch (err) {
340
+ if (json) {
341
+ throw new ArchiveBlockedError('archive_spec_update_failed', String(err.message || err), 'Fix the change delta specs and rerun. No files were changed.');
342
+ }
218
343
  console.log(String(err.message || err));
219
344
  console.log('Aborted. No files were changed.');
220
- return;
345
+ return null;
221
346
  }
222
- // All validations passed; pre-validate rebuilt full spec and then write files and display counts
223
- let totals = { added: 0, modified: 0, removed: 0, renamed: 0 };
224
- for (const p of prepared) {
225
- const specName = path.basename(path.dirname(p.update.target));
226
- if (!skipValidation) {
347
+ // Validate every rebuilt spec before writing any of them, so a
348
+ // late validation failure really does leave all targets unchanged.
349
+ if (!skipValidation) {
350
+ for (const p of prepared) {
351
+ const specName = path.basename(path.dirname(p.update.target));
227
352
  const report = await new Validator().validateSpecContent(specName, p.rebuilt);
228
353
  if (!report.valid) {
354
+ if (json) {
355
+ throw new ArchiveBlockedError('archive_spec_validation_failed', `Rebuilt spec for '${specName}' failed validation. No files were changed.`, `Run ${withStoreFlag(root, `openspec validate ${specName}`)} after fixing the change deltas.`);
356
+ }
229
357
  console.log(chalk.red(`\nValidation errors in rebuilt spec for ${specName} (will not write changes):`));
230
358
  for (const issue of report.issues) {
231
359
  if (issue.level === 'ERROR')
@@ -234,17 +362,29 @@ export class ArchiveCommand {
234
362
  console.log(chalk.yellow(` ⚠ ${issue.message}`));
235
363
  }
236
364
  console.log('Aborted. No files were changed.');
237
- return;
365
+ return null;
238
366
  }
239
367
  }
240
- await writeUpdatedSpec(p.update, p.rebuilt, p.counts);
241
- totals.added += p.counts.added;
242
- totals.modified += p.counts.modified;
243
- totals.removed += p.counts.removed;
244
- totals.renamed += p.counts.renamed;
245
368
  }
246
- console.log(`Totals: + ${totals.added}, ~ ${totals.modified}, - ${totals.removed}, → ${totals.renamed}`);
247
- console.log('Specs updated successfully.');
369
+ // All validations passed; write files and display counts
370
+ const writeTotals = { added: 0, modified: 0, removed: 0, renamed: 0 };
371
+ for (const p of prepared) {
372
+ await writeUpdatedSpec(p.update, p.rebuilt, p.counts, {
373
+ silent: json,
374
+ // Cross-root paths must be absolute when a store is selected.
375
+ ...(isStoreSelectedRoot(root) ? { displayPath: p.update.target } : {}),
376
+ });
377
+ writeTotals.added += p.counts.added;
378
+ writeTotals.modified += p.counts.modified;
379
+ writeTotals.removed += p.counts.removed;
380
+ writeTotals.renamed += p.counts.renamed;
381
+ }
382
+ specsUpdated = true;
383
+ totals = writeTotals;
384
+ if (!json) {
385
+ console.log(`Totals: + ${writeTotals.added}, ~ ${writeTotals.modified}, - ${writeTotals.removed}, → ${writeTotals.renamed}`);
386
+ console.log('Specs updated successfully.');
387
+ }
248
388
  }
249
389
  }
250
390
  }
@@ -252,20 +392,33 @@ export class ArchiveCommand {
252
392
  const archiveName = `${this.getArchiveDate()}-${changeName}`;
253
393
  const archivePath = path.join(archiveDir, archiveName);
254
394
  // Check if archive already exists
395
+ let archiveExists = false;
255
396
  try {
256
397
  await fs.access(archivePath);
257
- throw new Error(`Archive '${archiveName}' already exists.`);
398
+ archiveExists = true;
258
399
  }
259
400
  catch (error) {
260
401
  if (error.code !== 'ENOENT') {
261
402
  throw error;
262
403
  }
263
404
  }
405
+ if (archiveExists) {
406
+ throw new ArchiveBlockedError('archive_target_exists', `Archive '${archiveName}' already exists.`);
407
+ }
264
408
  // Create archive directory if needed
265
409
  await fs.mkdir(archiveDir, { recursive: true });
266
410
  // Move change to archive (uses copy+remove on EPERM/EXDEV, e.g. Windows)
267
411
  await moveDirectory(changeDir, archivePath);
268
- console.log(`Change '${changeName}' archived as '${archiveName}'.`);
412
+ if (!json) {
413
+ console.log(`Change '${changeName}' archived as '${archiveName}'.`);
414
+ }
415
+ return {
416
+ change: changeName,
417
+ archivedAs: archiveName,
418
+ path: archivePath,
419
+ specsUpdated,
420
+ ...(totals ? { totals } : {}),
421
+ };
269
422
  }
270
423
  async selectChange(changesDir) {
271
424
  const { select } = await import('@inquirer/prompts');
@@ -5,5 +5,5 @@ export { detectCompleted } from './state.js';
5
5
  export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
6
6
  export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, type SchemaInfo, } from './resolver.js';
7
7
  export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, type ChangeContext, type LoadChangeContextOptions, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, type ArtifactPathSummary, } from './instruction-loader.js';
8
- export type { PlanningHomeSummary, AffectedAreasSummary, ActionContext, } from '../change-status-policy.js';
8
+ export type { PlanningHomeSummary, ActionContext, } from '../change-status-policy.js';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,9 @@
1
1
  import { ArtifactGraph } from './graph.js';
2
- import { type ActionContext, type AffectedAreasSummary, type PlanningHomeSummary } from '../change-status-policy.js';
2
+ import { type ActionContext, type PlanningHomeSummary } from '../change-status-policy.js';
3
+ import { type ProjectConfig } from '../project-config.js';
4
+ import type { ReferenceIndexEntry } from '../references.js';
3
5
  import type { PlanningHome } from '../planning-home.js';
4
- import type { ChangeMetadata, InitiativeLink } from '../change-metadata/index.js';
6
+ import type { ChangeMetadata } from '../change-metadata/index.js';
5
7
  import type { CompletedSet } from './types.js';
6
8
  /**
7
9
  * Error thrown when loading a template fails.
@@ -30,8 +32,6 @@ export interface ChangeContext {
30
32
  planningHome?: PlanningHome;
31
33
  /** Parsed change metadata, when present */
32
34
  metadata?: ChangeMetadata;
33
- /** Stored initiative link, when this change is linked to shared context */
34
- initiative?: InitiativeLink;
35
35
  }
36
36
  export interface LoadChangeContextOptions {
37
37
  changeDir?: string;
@@ -51,8 +51,6 @@ export interface ArtifactInstructions {
51
51
  changeDir: string;
52
52
  /** Resolved planning home for this change */
53
53
  planningHome?: PlanningHomeSummary;
54
- /** Stored initiative link, when this change is linked to shared context */
55
- initiative?: InitiativeLink;
56
54
  /** Output path pattern (e.g., "proposal.md") */
57
55
  outputPath: string;
58
56
  /** Absolute output path or glob pattern resolved under the change directory */
@@ -67,6 +65,8 @@ export interface ArtifactInstructions {
67
65
  context: string | undefined;
68
66
  /** Artifact-specific rules from config (constraints for AI, not to be included in output) */
69
67
  rules: string[] | undefined;
68
+ /** Referenced-store index (read-only upstream context; omitted when no references are declared) */
69
+ references?: ReferenceIndexEntry[];
70
70
  /** Template content (structure to follow - this IS the output format) */
71
71
  template: string;
72
72
  /** Dependencies with completion status and paths */
@@ -108,16 +108,13 @@ export interface ChangeStatus {
108
108
  changeName: string;
109
109
  /** Schema name */
110
110
  schemaName: string;
111
- /** Resolved planning home for this change */
111
+ /** Planning home facts (generated skills derive the archive dir
112
+ * from planningHome.changesDir - a published agent contract). */
112
113
  planningHome?: PlanningHomeSummary;
113
- /** Stored initiative link, when this change is linked to shared context */
114
- initiative?: InitiativeLink;
115
114
  /** Full path to the change root */
116
115
  changeRoot: string;
117
116
  /** Absolute artifact path details keyed by artifact ID */
118
117
  artifactPaths: Record<string, ArtifactPathSummary>;
119
- /** Workspace affected-area summary, when available */
120
- affectedAreas?: AffectedAreasSummary;
121
118
  /** Plain-language next steps for users and agents */
122
119
  nextSteps: string[];
123
120
  /** Machine-readable action constraints for agents */
@@ -172,12 +169,20 @@ export declare function loadChangeContext(projectRoot: string, changeName: strin
172
169
  * @returns Enriched artifact instructions
173
170
  * @throws Error if artifact not found
174
171
  */
175
- export declare function generateInstructions(context: ChangeContext, artifactId: string, projectRoot?: string): ArtifactInstructions;
172
+ export interface GenerateInstructionsOptions {
173
+ /** Pre-read project config; suppresses the internal read (no double read). */
174
+ projectConfig?: ProjectConfig | null;
175
+ /** Referenced-store index assembled at the command boundary. */
176
+ references?: ReferenceIndexEntry[];
177
+ }
178
+ export declare function generateInstructions(context: ChangeContext, artifactId: string, projectRoot?: string, options?: GenerateInstructionsOptions): ArtifactInstructions;
176
179
  /**
177
180
  * Formats the status of all artifacts in a change.
178
181
  *
179
182
  * @param context - Change context
180
183
  * @returns Formatted change status
181
184
  */
182
- export declare function formatChangeStatus(context: ChangeContext): ChangeStatus;
185
+ export declare function formatChangeStatus(context: ChangeContext, options?: {
186
+ storeId?: string;
187
+ }): ChangeStatus;
183
188
  //# sourceMappingURL=instruction-loader.d.ts.map
@@ -6,7 +6,7 @@ import { detectCompleted } from './state.js';
6
6
  import { resolveArtifactOutputs } from './outputs.js';
7
7
  import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
8
8
  import { FileSystemUtils } from '../../utils/file-system.js';
9
- import { buildActionContext, buildNextSteps, summarizeAffectedAreas, summarizePlanningHome, } from '../change-status-policy.js';
9
+ import { buildActionContext, buildNextSteps, summarizePlanningHome, } from '../change-status-policy.js';
10
10
  import { readProjectConfig, validateConfigRules } from '../project-config.js';
11
11
  // Session-level cache for validation warnings (avoid repeating same warnings)
12
12
  const shownWarnings = new Set();
@@ -79,24 +79,9 @@ export function loadChangeContext(projectRoot, changeName, schemaName, options =
79
79
  projectRoot,
80
80
  ...(options.planningHome ? { planningHome: options.planningHome } : {}),
81
81
  ...(metadata ? { metadata } : {}),
82
- ...(metadata?.initiative ? { initiative: metadata.initiative } : {}),
83
82
  };
84
83
  }
85
- /**
86
- * Generates enriched instructions for creating an artifact.
87
- *
88
- * Instruction injection order:
89
- * 1. <context> - Project context from config (if present)
90
- * 2. <rules> - Artifact-specific rules from config (if present)
91
- * 3. <template> - Schema's template content
92
- *
93
- * @param context - Change context
94
- * @param artifactId - Artifact ID to generate instructions for
95
- * @param projectRoot - Project root directory (for reading config)
96
- * @returns Enriched artifact instructions
97
- * @throws Error if artifact not found
98
- */
99
- export function generateInstructions(context, artifactId, projectRoot) {
84
+ export function generateInstructions(context, artifactId, projectRoot, options = {}) {
100
85
  const artifact = context.graph.getArtifact(artifactId);
101
86
  if (!artifact) {
102
87
  throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
@@ -106,9 +91,9 @@ export function generateInstructions(context, artifactId, projectRoot) {
106
91
  const unlocks = getUnlockedArtifacts(context.graph, artifactId);
107
92
  // Use projectRoot from context if not explicitly provided
108
93
  const effectiveProjectRoot = projectRoot ?? context.projectRoot;
109
- // Try to read project config for context and rules
110
- let projectConfig = null;
111
- if (effectiveProjectRoot) {
94
+ // Use the pre-read config when provided; otherwise read it here.
95
+ let projectConfig = options.projectConfig ?? null;
96
+ if (options.projectConfig === undefined && effectiveProjectRoot) {
112
97
  try {
113
98
  projectConfig = readProjectConfig(effectiveProjectRoot);
114
99
  }
@@ -138,7 +123,6 @@ export function generateInstructions(context, artifactId, projectRoot) {
138
123
  schemaName: context.schemaName,
139
124
  changeDir: context.changeDir,
140
125
  planningHome: summarizePlanningHome(context.planningHome),
141
- ...(context.initiative ? { initiative: context.initiative } : {}),
142
126
  outputPath: artifact.generates,
143
127
  resolvedOutputPath: path.join(context.changeDir, artifact.generates),
144
128
  existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
@@ -146,6 +130,7 @@ export function generateInstructions(context, artifactId, projectRoot) {
146
130
  instruction: artifact.instruction,
147
131
  context: configContext,
148
132
  rules: configRules,
133
+ ...(options.references !== undefined ? { references: options.references } : {}),
149
134
  template: templateContent,
150
135
  dependencies,
151
136
  unlocks,
@@ -183,7 +168,7 @@ function getUnlockedArtifacts(graph, artifactId) {
183
168
  * @param context - Change context
184
169
  * @returns Formatted change status
185
170
  */
186
- export function formatChangeStatus(context) {
171
+ export function formatChangeStatus(context, options = {}) {
187
172
  // Load schema to get apply phase configuration
188
173
  const schema = resolveSchema(context.schemaName, context.projectRoot);
189
174
  const applyRequires = schema.apply?.requires ?? schema.artifacts.map(a => a.id);
@@ -222,31 +207,23 @@ export function formatChangeStatus(context) {
222
207
  const buildOrder = context.graph.getBuildOrder();
223
208
  const orderMap = new Map(buildOrder.map((id, idx) => [id, idx]));
224
209
  artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0));
225
- const affectedAreas = summarizeAffectedAreas({
226
- planningHome: context.planningHome,
227
- metadata: context.metadata,
228
- });
229
210
  const isComplete = context.graph.isComplete(context.completed);
230
211
  const artifactIds = artifactStatuses.map((artifact) => artifact.id);
231
212
  return {
232
213
  changeName: context.changeName,
233
214
  schemaName: context.schemaName,
234
215
  planningHome: summarizePlanningHome(context.planningHome),
235
- ...(context.initiative ? { initiative: context.initiative } : {}),
236
216
  changeRoot: context.changeDir,
237
217
  artifactPaths,
238
- affectedAreas,
239
218
  isComplete,
240
219
  applyRequires,
241
220
  nextSteps: buildNextSteps({
242
221
  changeName: context.changeName,
243
- planningHome: context.planningHome,
244
222
  artifactStatuses,
245
- affectedAreas,
246
223
  allArtifactsComplete: isComplete,
224
+ ...(options.storeId ? { storeId: options.storeId } : {}),
247
225
  }),
248
226
  actionContext: buildActionContext({
249
- planningHome: context.planningHome,
250
227
  projectRoot: context.projectRoot,
251
228
  artifactIds,
252
229
  }),
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ export { isKebabId } from '../id.js';
2
3
  export declare const InitiativeLinkSchema: z.ZodObject<{
3
4
  store: z.ZodString;
4
5
  id: z.ZodString;
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
+ import { isKebabId } from '../id.js';
3
+ export { isKebabId } from '../id.js';
2
4
  const KebabIdentifierSchema = (label) => z.string().superRefine((value, ctx) => {
3
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(value)) {
5
+ if (!isKebabId(value)) {
4
6
  ctx.addIssue({
5
7
  code: 'custom',
6
8
  message: `${label} must be kebab-case with lowercase letters, numbers, and single hyphen separators`,
@@ -8,7 +10,7 @@ const KebabIdentifierSchema = (label) => z.string().superRefine((value, ctx) =>
8
10
  }
9
11
  });
10
12
  export const InitiativeLinkSchema = z.object({
11
- store: KebabIdentifierSchema('Context store id'),
13
+ store: KebabIdentifierSchema('Store id'),
12
14
  id: KebabIdentifierSchema('Initiative id'),
13
15
  }).strict();
14
16
  // Per-change metadata schema. The schema field is validated against available