@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
@@ -3,7 +3,59 @@ 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
+ function isMissingPathError(error) {
9
+ return (typeof error === 'object' &&
10
+ error !== null &&
11
+ 'code' in error &&
12
+ error.code === 'ENOENT');
13
+ }
14
+ async function listActiveChangeNames(changesDir) {
15
+ try {
16
+ const entries = await fs.readdir(changesDir, { withFileTypes: true });
17
+ return entries
18
+ .filter((entry) => entry.isDirectory() && entry.name !== 'archive')
19
+ .map((entry) => entry.name)
20
+ .sort();
21
+ }
22
+ catch (error) {
23
+ if (!isMissingPathError(error))
24
+ throw error;
25
+ return [];
26
+ }
27
+ }
28
+ /**
29
+ * JSON mode is non-interactive: any point where the human flow would prompt or
30
+ * print prose instead throws this error, which becomes a machine-readable
31
+ * status entry with a non-zero exit code.
32
+ */
33
+ class ArchiveBlockedError extends Error {
34
+ diagnostic;
35
+ constructor(code, message, fix) {
36
+ super(message);
37
+ this.name = 'ArchiveBlockedError';
38
+ this.diagnostic = {
39
+ severity: 'error',
40
+ code,
41
+ message,
42
+ ...(fix ? { fix } : {}),
43
+ };
44
+ }
45
+ }
46
+ function toArchiveDiagnostic(error) {
47
+ if (error instanceof ArchiveBlockedError) {
48
+ return error.diagnostic;
49
+ }
50
+ if (isRootSelectionError(error)) {
51
+ return error.diagnostic;
52
+ }
53
+ return {
54
+ severity: 'error',
55
+ code: 'archive_error',
56
+ message: error instanceof Error ? error.message : String(error),
57
+ };
58
+ }
7
59
  /**
8
60
  * Recursively copy a directory. Used when fs.rename fails (e.g. EPERM on Windows).
9
61
  */
@@ -44,23 +96,63 @@ async function moveDirectory(src, dest) {
44
96
  }
45
97
  export class ArchiveCommand {
46
98
  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');
51
- // Check if changes directory exists
99
+ const json = !!options.json;
100
+ let root;
52
101
  try {
53
- await fs.access(changesDir);
102
+ root = await resolveOpenSpecRoot({
103
+ ...(options.store !== undefined ? { store: options.store } : {}),
104
+ ...(options.storePath !== undefined ? { storePath: options.storePath } : {}),
105
+ });
54
106
  }
55
- catch {
56
- throw new Error("No OpenSpec changes directory found. Run 'openspec init' first.");
107
+ catch (error) {
108
+ if (json && isRootSelectionError(error)) {
109
+ this.printJsonFailure(undefined, toArchiveDiagnostic(error));
110
+ return;
111
+ }
112
+ throw error;
57
113
  }
114
+ if (json) {
115
+ try {
116
+ const result = await this.run(changeName, options, root, true);
117
+ if (!result) {
118
+ return;
119
+ }
120
+ console.log(JSON.stringify({ archive: result, root: toRootOutput(root) }, null, 2));
121
+ }
122
+ catch (error) {
123
+ this.printJsonFailure(root, toArchiveDiagnostic(error));
124
+ }
125
+ return;
126
+ }
127
+ emitStoreRootBanner(root);
128
+ await this.run(changeName, options, root, false);
129
+ }
130
+ printJsonFailure(root, diagnostic) {
131
+ console.log(JSON.stringify({
132
+ archive: null,
133
+ ...(root ? { root: toRootOutput(root) } : {}),
134
+ status: [diagnostic],
135
+ }, null, 2));
136
+ process.exitCode = 1;
137
+ }
138
+ /**
139
+ * Shared archive flow. In human mode (json=false) prompts and prose match
140
+ * the historical behavior and cancellations return null. In JSON mode no
141
+ * prose reaches stdout and every blocked path throws.
142
+ */
143
+ async run(changeName, options, root, json) {
144
+ const changesDir = root.changesDir;
145
+ const archiveDir = root.archiveDir;
146
+ const mainSpecsDir = root.specsDir;
58
147
  // Get change name interactively if not provided
59
148
  if (!changeName) {
149
+ if (json) {
150
+ throw new ArchiveBlockedError('archive_change_name_required', 'A change name is required: archive --json is non-interactive.', withStoreFlag(root, 'openspec archive <change-name> --json'));
151
+ }
60
152
  const selectedChange = await this.selectChange(changesDir);
61
153
  if (!selectedChange) {
62
154
  console.log('No change selected. Aborting.');
63
- return;
155
+ return null;
64
156
  }
65
157
  changeName = selectedChange;
66
158
  }
@@ -73,29 +165,34 @@ export class ArchiveCommand {
73
165
  }
74
166
  }
75
167
  catch {
76
- throw new Error(`Change '${changeName}' not found.`);
168
+ const available = await listActiveChangeNames(changesDir);
169
+ throw new ArchiveBlockedError('archive_change_not_found', available.length > 0
170
+ ? `Change '${changeName}' not found. Available changes: ${available.join(', ')}`
171
+ : `Change '${changeName}' not found. No active changes exist in this root.`);
77
172
  }
78
173
  const skipValidation = options.validate === false || options.noValidate === true;
79
174
  // Validate specs and change before archiving
80
175
  if (!skipValidation) {
81
176
  const validator = new Validator();
82
177
  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}`));
178
+ // Validate proposal.md (informative only; human mode prints warnings)
179
+ if (!json) {
180
+ const changeFile = path.join(changeDir, 'proposal.md');
181
+ try {
182
+ await fs.access(changeFile);
183
+ const changeReport = await validator.validateChange(changeFile);
184
+ // Proposal validation is informative only (do not block archive)
185
+ if (!changeReport.valid) {
186
+ console.log(chalk.yellow(`\nProposal warnings in proposal.md (non-blocking):`));
187
+ for (const issue of changeReport.issues) {
188
+ const symbol = issue.level === 'ERROR' ? '⚠' : (issue.level === 'WARNING' ? '⚠' : 'ℹ');
189
+ console.log(chalk.yellow(` ${symbol} ${issue.message}`));
190
+ }
94
191
  }
95
192
  }
96
- }
97
- catch {
98
- // Change file doesn't exist, skip validation
193
+ catch {
194
+ // Change file doesn't exist, skip validation
195
+ }
99
196
  }
100
197
  // Validate delta-formatted spec files under the change directory if present
101
198
  const changeSpecsDir = path.join(changeDir, 'specs');
@@ -122,21 +219,32 @@ export class ArchiveCommand {
122
219
  const deltaReport = await validator.validateChangeDeltaSpecs(changeDir);
123
220
  if (!deltaReport.valid) {
124
221
  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}`));
222
+ if (!json) {
223
+ console.log(chalk.red(`\nValidation errors in change delta specs:`));
224
+ for (const issue of deltaReport.issues) {
225
+ if (issue.level === 'ERROR') {
226
+ console.log(chalk.red(` ✗ ${issue.message}`));
227
+ }
228
+ else if (issue.level === 'WARNING') {
229
+ console.log(chalk.yellow(` ⚠ ${issue.message}`));
230
+ }
132
231
  }
133
232
  }
134
233
  }
135
234
  }
136
235
  if (hasValidationErrors) {
236
+ if (json) {
237
+ 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.`);
238
+ }
137
239
  console.log(chalk.red('\nValidation failed. Please fix the errors before archiving.'));
138
240
  console.log(chalk.yellow('To skip validation (not recommended), use --no-validate flag.'));
139
- return;
241
+ process.exitCode = 1;
242
+ return null;
243
+ }
244
+ }
245
+ else if (json) {
246
+ if (!options.yes) {
247
+ throw new ArchiveBlockedError('archive_confirmation_required', 'Skipping validation requires confirmation: rerun with --yes.', withStoreFlag(root, 'openspec archive <change-name> --json --no-validate --yes'));
140
248
  }
141
249
  }
142
250
  else {
@@ -150,7 +258,7 @@ export class ArchiveCommand {
150
258
  });
151
259
  if (!proceed) {
152
260
  console.log('Archive cancelled.');
153
- return;
261
+ return null;
154
262
  }
155
263
  }
156
264
  else {
@@ -160,12 +268,19 @@ export class ArchiveCommand {
160
268
  console.log(chalk.yellow(`Affected files: ${changeDir}`));
161
269
  }
162
270
  // Show progress and check for incomplete tasks
163
- const progress = await getTaskProgressForChange(changesDir, changeName);
164
- const status = formatTaskStatus(progress);
165
- console.log(`Task status: ${status}`);
271
+ const progress = await getTaskProgressForChange(changesDir, changeName, path.resolve(changesDir, '..', '..'));
272
+ if (!json) {
273
+ const status = formatTaskStatus(progress);
274
+ console.log(`Task status: ${status}`);
275
+ }
166
276
  const incompleteTasks = Math.max(progress.total - progress.completed, 0);
167
277
  if (incompleteTasks > 0) {
168
- if (!options.yes) {
278
+ if (json) {
279
+ if (!options.yes) {
280
+ throw new ArchiveBlockedError('archive_tasks_incomplete', `${incompleteTasks} incomplete task(s) found for change '${changeName}'.`, 'Complete the tasks or rerun with --yes.');
281
+ }
282
+ }
283
+ else if (!options.yes) {
169
284
  const { confirm } = await import('@inquirer/prompts');
170
285
  const proceed = await confirm({
171
286
  message: `Warning: ${incompleteTasks} incomplete task(s) found. Continue?`,
@@ -173,7 +288,7 @@ export class ArchiveCommand {
173
288
  });
174
289
  if (!proceed) {
175
290
  console.log('Archive cancelled.');
176
- return;
291
+ return null;
177
292
  }
178
293
  }
179
294
  else {
@@ -181,21 +296,30 @@ export class ArchiveCommand {
181
296
  }
182
297
  }
183
298
  // Handle spec updates unless skipSpecs flag is set
299
+ let specsUpdated = false;
300
+ let totals;
184
301
  if (options.skipSpecs) {
185
- console.log('Skipping spec updates (--skip-specs flag provided).');
302
+ if (!json) {
303
+ console.log('Skipping spec updates (--skip-specs flag provided).');
304
+ }
186
305
  }
187
306
  else {
188
307
  // Find specs to update
189
308
  const specUpdates = await findSpecUpdates(changeDir, mainSpecsDir);
190
309
  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}`);
310
+ if (!json) {
311
+ console.log('\nSpecs to update:');
312
+ for (const update of specUpdates) {
313
+ const status = update.exists ? 'update' : 'create';
314
+ const capability = path.basename(path.dirname(update.target));
315
+ console.log(` ${capability}: ${status}`);
316
+ }
196
317
  }
197
318
  let shouldUpdateSpecs = true;
198
319
  if (!options.yes) {
320
+ if (json) {
321
+ throw new ArchiveBlockedError('archive_confirmation_required', `Updating ${specUpdates.length} spec(s) requires confirmation: rerun with --yes.`, withStoreFlag(root, 'openspec archive <change-name> --json --yes'));
322
+ }
199
323
  const { confirm } = await import('@inquirer/prompts');
200
324
  shouldUpdateSpecs = await confirm({
201
325
  message: 'Proceed with spec updates?',
@@ -210,22 +334,29 @@ export class ArchiveCommand {
210
334
  const prepared = [];
211
335
  try {
212
336
  for (const update of specUpdates) {
213
- const built = await buildUpdatedSpec(update, changeName);
337
+ const built = await buildUpdatedSpec(update, changeName, { silent: json });
214
338
  prepared.push({ update, rebuilt: built.rebuilt, counts: built.counts });
215
339
  }
216
340
  }
217
341
  catch (err) {
342
+ if (json) {
343
+ throw new ArchiveBlockedError('archive_spec_update_failed', String(err.message || err), 'Fix the change delta specs and rerun. No files were changed.');
344
+ }
218
345
  console.log(String(err.message || err));
219
346
  console.log('Aborted. No files were changed.');
220
- return;
347
+ process.exitCode = 1;
348
+ return null;
221
349
  }
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) {
350
+ // Validate every rebuilt spec before writing any of them, so a
351
+ // late validation failure really does leave all targets unchanged.
352
+ if (!skipValidation) {
353
+ for (const p of prepared) {
354
+ const specName = path.basename(path.dirname(p.update.target));
227
355
  const report = await new Validator().validateSpecContent(specName, p.rebuilt);
228
356
  if (!report.valid) {
357
+ if (json) {
358
+ 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.`);
359
+ }
229
360
  console.log(chalk.red(`\nValidation errors in rebuilt spec for ${specName} (will not write changes):`));
230
361
  for (const issue of report.issues) {
231
362
  if (issue.level === 'ERROR')
@@ -234,17 +365,30 @@ export class ArchiveCommand {
234
365
  console.log(chalk.yellow(` ⚠ ${issue.message}`));
235
366
  }
236
367
  console.log('Aborted. No files were changed.');
237
- return;
368
+ process.exitCode = 1;
369
+ return null;
238
370
  }
239
371
  }
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
372
  }
246
- console.log(`Totals: + ${totals.added}, ~ ${totals.modified}, - ${totals.removed}, → ${totals.renamed}`);
247
- console.log('Specs updated successfully.');
373
+ // All validations passed; write files and display counts
374
+ const writeTotals = { added: 0, modified: 0, removed: 0, renamed: 0 };
375
+ for (const p of prepared) {
376
+ await writeUpdatedSpec(p.update, p.rebuilt, p.counts, {
377
+ silent: json,
378
+ // Cross-root paths must be absolute when a store is selected.
379
+ ...(isStoreSelectedRoot(root) ? { displayPath: p.update.target } : {}),
380
+ });
381
+ writeTotals.added += p.counts.added;
382
+ writeTotals.modified += p.counts.modified;
383
+ writeTotals.removed += p.counts.removed;
384
+ writeTotals.renamed += p.counts.renamed;
385
+ }
386
+ specsUpdated = true;
387
+ totals = writeTotals;
388
+ if (!json) {
389
+ console.log(`Totals: + ${writeTotals.added}, ~ ${writeTotals.modified}, - ${writeTotals.removed}, → ${writeTotals.renamed}`);
390
+ console.log('Specs updated successfully.');
391
+ }
248
392
  }
249
393
  }
250
394
  }
@@ -252,29 +396,37 @@ export class ArchiveCommand {
252
396
  const archiveName = `${this.getArchiveDate()}-${changeName}`;
253
397
  const archivePath = path.join(archiveDir, archiveName);
254
398
  // Check if archive already exists
399
+ let archiveExists = false;
255
400
  try {
256
401
  await fs.access(archivePath);
257
- throw new Error(`Archive '${archiveName}' already exists.`);
402
+ archiveExists = true;
258
403
  }
259
404
  catch (error) {
260
405
  if (error.code !== 'ENOENT') {
261
406
  throw error;
262
407
  }
263
408
  }
409
+ if (archiveExists) {
410
+ throw new ArchiveBlockedError('archive_target_exists', `Archive '${archiveName}' already exists.`);
411
+ }
264
412
  // Create archive directory if needed
265
413
  await fs.mkdir(archiveDir, { recursive: true });
266
414
  // Move change to archive (uses copy+remove on EPERM/EXDEV, e.g. Windows)
267
415
  await moveDirectory(changeDir, archivePath);
268
- console.log(`Change '${changeName}' archived as '${archiveName}'.`);
416
+ if (!json) {
417
+ console.log(`Change '${changeName}' archived as '${archiveName}'.`);
418
+ }
419
+ return {
420
+ change: changeName,
421
+ archivedAs: archiveName,
422
+ path: archivePath,
423
+ specsUpdated,
424
+ ...(totals ? { totals } : {}),
425
+ };
269
426
  }
270
427
  async selectChange(changesDir) {
271
428
  const { select } = await import('@inquirer/prompts');
272
- // Get all directories in changes (excluding archive)
273
- const entries = await fs.readdir(changesDir, { withFileTypes: true });
274
- const changeDirs = entries
275
- .filter(entry => entry.isDirectory() && entry.name !== 'archive')
276
- .map(entry => entry.name)
277
- .sort();
429
+ const changeDirs = await listActiveChangeNames(changesDir);
278
430
  if (changeDirs.length === 0) {
279
431
  console.log('No active changes found.');
280
432
  return null;
@@ -284,7 +436,7 @@ export class ArchiveCommand {
284
436
  try {
285
437
  const progressList = [];
286
438
  for (const id of changeDirs) {
287
- const progress = await getTaskProgressForChange(changesDir, id);
439
+ const progress = await getTaskProgressForChange(changesDir, id, path.resolve(changesDir, '..', '..'));
288
440
  const status = formatTaskStatus(progress);
289
441
  progressList.push({ id, status });
290
442
  }
@@ -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