@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
package/README.md CHANGED
@@ -47,6 +47,14 @@ Our philosophy:
47
47
  ## See it in action
48
48
 
49
49
  ```text
50
+ You: /opsx:explore
51
+ AI: What would you like to explore?
52
+ You: I want dark mode but I'm not sure how to do it cleanly.
53
+ AI: Let me look at your styling setup...
54
+ Cleanest path here: CSS variables + a small theme context,
55
+ with system-preference detection. No new dependencies. Scope it?
56
+ You: Yes, let's do it.
57
+
50
58
  You: /opsx:propose add-dark-mode
51
59
  AI: Created openspec/changes/add-dark-mode/
52
60
  ✓ proposal.md — why we're doing this, what's changing
@@ -94,9 +102,12 @@ cd your-project
94
102
  openspec init
95
103
  ```
96
104
 
97
- Now tell your AI: `/opsx:propose <what-you-want-to-build>`
105
+ Now talk to your AI:
106
+
107
+ - **Not sure what to build yet?** Start with `/opsx:explore`, a no-stakes thinking partner that reads your code, weighs options, and shapes a plan before anything is written. ([Explore guide](docs/explore.md))
108
+ - **Already know what you want?** Go straight to `/opsx:propose <what-you-want-to-build>`.
98
109
 
99
- If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
110
+ Both are in the default profile. If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
100
111
 
101
112
  > [!NOTE]
102
113
  > Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
@@ -105,14 +116,24 @@ If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/
105
116
 
106
117
  ## Docs
107
118
 
119
+ **Start here:** the **[Documentation Home](docs/README.md)** maps everything. New to OpenSpec? Read [Getting Started](docs/getting-started.md), then [How Commands Work](docs/how-commands-work.md) (where you actually type `/opsx:propose`).
120
+
108
121
  → **[Getting Started](docs/getting-started.md)**: first steps<br>
122
+ → **[Explore First](docs/explore.md)**: think it through with `/opsx:explore` before you commit<br>
123
+ → **[How Commands Work](docs/how-commands-work.md)**: where slash commands run vs the CLI<br>
124
+ → **[Core Concepts at a Glance](docs/overview.md)**: the whole mental model, one page<br>
125
+ → **[Examples & Recipes](docs/examples.md)**: real changes, start to finish<br>
109
126
  → **[Workflows](docs/workflows.md)**: combos and patterns<br>
127
+ → **[Existing Projects](docs/existing-projects.md)**: adopt OpenSpec on a brownfield codebase<br>
128
+ → **[Editing a Change](docs/editing-changes.md)**: update artifacts, go back, reconcile manual edits<br>
110
129
  → **[Commands](docs/commands.md)**: slash commands & skills<br>
111
130
  → **[CLI](docs/cli.md)**: terminal reference<br>
131
+ → **[Stores](docs/stores-beta/user-guide.md)**: plan in a separate repo, shared across your team (beta)<br>
112
132
  → **[Supported Tools](docs/supported-tools.md)**: tool integrations & install paths<br>
113
133
  → **[Concepts](docs/concepts.md)**: how it all fits<br>
114
134
  → **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
115
- → **[Customization](docs/customization.md)**: make it yours
135
+ → **[Customization](docs/customization.md)**: make it yours<br>
136
+ → **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
116
137
 
117
138
 
118
139
  ## Community schemas
@@ -1,5 +1,10 @@
1
1
  import { Command } from 'commander';
2
2
  declare const program: Command;
3
+ /**
4
+ * Get the full command path for nested commands.
5
+ * For example: 'change show' -> 'change:show'
6
+ */
7
+ export declare function getCommandPath(command: Command): string;
3
8
  export { program };
4
9
  export declare function runCli(argv?: string[]): void;
5
10
  //# sourceMappingURL=index.d.ts.map
package/dist/cli/index.js CHANGED
@@ -1,14 +1,16 @@
1
- import { Command } from 'commander';
1
+ import { asStatus } from '../commands/shared-output.js';
2
+ import { Command, Option } from 'commander';
2
3
  import { createRequire } from 'module';
3
4
  import ora from 'ora';
4
5
  import path from 'path';
5
6
  import { fileURLToPath } from 'url';
6
7
  import { promises as fs } from 'fs';
7
- import { AI_TOOLS, OPENSPEC_DIR_NAME } from '../core/config.js';
8
+ import { AI_TOOLS } from '../core/config.js';
8
9
  import { UpdateCommand } from '../core/update.js';
9
10
  import { ListCommand } from '../core/list.js';
10
11
  import { ArchiveCommand } from '../core/archive.js';
11
12
  import { ViewCommand } from '../core/view.js';
13
+ import { resolveRootForCommand, toRootOutput } from '../core/root-selection.js';
12
14
  import { registerSpecCommand } from '../commands/spec.js';
13
15
  import { ChangeCommand } from '../commands/change.js';
14
16
  import { ValidateCommand } from '../commands/validate.js';
@@ -17,12 +19,37 @@ import { CompletionCommand } from '../commands/completion.js';
17
19
  import { FeedbackCommand } from '../commands/feedback.js';
18
20
  import { registerConfigCommand } from '../commands/config.js';
19
21
  import { registerSchemaCommand } from '../commands/schema.js';
20
- import { registerWorkspaceCommand } from '../commands/workspace.js';
21
- import { registerContextStoreCommand } from '../commands/context-store.js';
22
- import { registerInitiativeCommand } from '../commands/initiative.js';
23
- import { findWorkspaceRoot } from '../core/workspace/index.js';
24
- import { statusCommand, instructionsCommand, applyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, setChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
22
+ import { registerStoreCommand } from '../commands/store.js';
23
+ import { registerDoctorCommand } from '../commands/doctor.js';
24
+ import { registerContextCommand } from '../commands/context.js';
25
+ import { registerWorksetCommand } from '../commands/workset.js';
26
+ import { statusCommand, instructionsCommand, applyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
25
27
  import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
28
+ import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
29
+ const STORE_OPTION_DESCRIPTION = COMMON_FLAGS.store.description;
30
+ // Deliberate rejection path: --store-path stays registered (hidden) so the
31
+ // resolver can explain that registering the path is the supported route,
32
+ // instead of Commander emitting a generic unknown-option error (or, for
33
+ // `show`, silently ignoring it via allowUnknownOption).
34
+ function hiddenStorePathOption() {
35
+ return new Option('--store-path <path>', 'Not supported; register the path with "openspec store register <path>" and use --store <id>').hideHelp();
36
+ }
37
+ function failWithError(error, json) {
38
+ // The agent contract: every --json failure leaves exactly one JSON
39
+ // document on stdout (the command's null-shape plus a status array).
40
+ if (json?.enabled) {
41
+ console.log(JSON.stringify({ ...(json.payload ?? {}), status: [asStatus(error, json.fallbackCode ?? 'command_error')] }, null, 2));
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+ ora().fail(`Error: ${error.message}`);
46
+ // Resolution and store errors carry a pasteable fix - never drop it.
47
+ const fix = error.diagnostic?.fix;
48
+ if (fix) {
49
+ console.error(`Fix: ${fix}`);
50
+ }
51
+ process.exitCode = process.exitCode ?? 1;
52
+ }
26
53
  const program = new Command();
27
54
  const require = createRequire(import.meta.url);
28
55
  const { version } = require('../../package.json');
@@ -30,7 +57,7 @@ const { version } = require('../../package.json');
30
57
  * Get the full command path for nested commands.
31
58
  * For example: 'change show' -> 'change:show'
32
59
  */
33
- function getCommandPath(command) {
60
+ export function getCommandPath(command) {
34
61
  const names = [];
35
62
  let current = command;
36
63
  while (current) {
@@ -70,21 +97,6 @@ program.hook('postAction', async () => {
70
97
  });
71
98
  const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value);
72
99
  const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
73
- async function hasRepoLocalOpenSpecProject(projectPath) {
74
- try {
75
- const stats = await fs.stat(path.join(projectPath, OPENSPEC_DIR_NAME));
76
- return stats.isDirectory();
77
- }
78
- catch (error) {
79
- const code = typeof error === 'object' && error !== null && 'code' in error
80
- ? error.code
81
- : undefined;
82
- if (code !== 'ENOENT' && code !== 'ENOTDIR') {
83
- throw error;
84
- }
85
- return false;
86
- }
87
- }
88
100
  program
89
101
  .command('init [path]')
90
102
  .description('Initialize OpenSpec in your project')
@@ -122,8 +134,7 @@ program
122
134
  await initCommand.execute(targetPath);
123
135
  }
124
136
  catch (error) {
125
- console.log(); // Empty line for spacing
126
- ora().fail(`Error: ${error.message}`);
137
+ failWithError(error);
127
138
  process.exit(1);
128
139
  }
129
140
  });
@@ -144,8 +155,7 @@ program
144
155
  await initCommand.execute('.');
145
156
  }
146
157
  catch (error) {
147
- console.log();
148
- ora().fail(`Error: ${error.message}`);
158
+ failWithError(error);
149
159
  process.exit(1);
150
160
  }
151
161
  });
@@ -155,21 +165,11 @@ program
155
165
  .option('--force', 'Force update even when tools are up to date')
156
166
  .action(async (targetPath = '.', options) => {
157
167
  try {
158
- const resolvedPath = path.resolve(targetPath);
159
168
  const updateCommand = new UpdateCommand({ force: options?.force });
160
- if (await hasRepoLocalOpenSpecProject(resolvedPath)) {
161
- await updateCommand.execute(resolvedPath);
162
- return;
163
- }
164
- const workspaceRoot = await findWorkspaceRoot(resolvedPath);
165
- if (workspaceRoot) {
166
- throw new Error('OpenSpec workspace detected. Run `openspec workspace update` to refresh workspace-local guidance and skills.');
167
- }
168
- await updateCommand.execute(resolvedPath);
169
+ await updateCommand.execute(targetPath);
169
170
  }
170
171
  catch (error) {
171
- console.log(); // Empty line for spacing
172
- ora().fail(`Error: ${error.message}`);
172
+ failWithError(error);
173
173
  process.exit(1);
174
174
  }
175
175
  });
@@ -180,16 +180,32 @@ program
180
180
  .option('--changes', 'List changes explicitly (default)')
181
181
  .option('--sort <order>', 'Sort order: "recent" (default) or "name"', 'recent')
182
182
  .option('--json', 'Output as JSON (for programmatic use)')
183
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
184
+ .addOption(hiddenStorePathOption())
183
185
  .action(async (options) => {
184
186
  try {
187
+ const root = await resolveRootForCommand(options ?? {}, {
188
+ json: options?.json,
189
+ failurePayload: options?.specs ? { specs: [], root: null } : { changes: [], root: null },
190
+ });
191
+ if (!root) {
192
+ return;
193
+ }
185
194
  const listCommand = new ListCommand();
186
195
  const mode = options?.specs ? 'specs' : 'changes';
187
196
  const sort = options?.sort === 'name' ? 'name' : 'recent';
188
- await listCommand.execute('.', mode, { sort, json: options?.json });
197
+ await listCommand.execute(root.path, mode, {
198
+ sort,
199
+ json: options?.json,
200
+ ...(options?.json ? { root: toRootOutput(root) } : {}),
201
+ });
189
202
  }
190
203
  catch (error) {
191
- console.log(); // Empty line for spacing
192
- ora().fail(`Error: ${error.message}`);
204
+ failWithError(error, {
205
+ enabled: options?.json,
206
+ payload: options?.specs ? { specs: [], root: null } : { changes: [], root: null },
207
+ fallbackCode: 'list_error',
208
+ });
193
209
  process.exit(1);
194
210
  }
195
211
  });
@@ -202,8 +218,7 @@ program
202
218
  await viewCommand.execute('.');
203
219
  }
204
220
  catch (error) {
205
- console.log(); // Empty line for spacing
206
- ora().fail(`Error: ${error.message}`);
221
+ failWithError(error);
207
222
  process.exit(1);
208
223
  }
209
224
  });
@@ -273,23 +288,26 @@ program
273
288
  .option('-y, --yes', 'Skip confirmation prompts')
274
289
  .option('--skip-specs', 'Skip spec update operations (useful for infrastructure, tooling, or doc-only changes)')
275
290
  .option('--no-validate', 'Skip validation (not recommended, requires confirmation)')
291
+ .option('--json', 'Output as JSON (non-interactive)')
292
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
293
+ .addOption(hiddenStorePathOption())
276
294
  .action(async (changeName, options) => {
277
295
  try {
278
296
  const archiveCommand = new ArchiveCommand();
279
297
  await archiveCommand.execute(changeName, options);
280
298
  }
281
299
  catch (error) {
282
- console.log(); // Empty line for spacing
283
- ora().fail(`Error: ${error.message}`);
300
+ failWithError(error);
284
301
  process.exit(1);
285
302
  }
286
303
  });
287
304
  registerSpecCommand(program);
288
305
  registerConfigCommand(program);
289
306
  registerSchemaCommand(program);
290
- registerWorkspaceCommand(program);
291
- registerContextStoreCommand(program);
292
- registerInitiativeCommand(program);
307
+ registerStoreCommand(program);
308
+ registerDoctorCommand(program);
309
+ registerContextCommand(program);
310
+ registerWorksetCommand(program);
293
311
  // Top-level validate command
294
312
  program
295
313
  .command('validate [item-name]')
@@ -302,14 +320,15 @@ program
302
320
  .option('--json', 'Output validation results as JSON')
303
321
  .option('--concurrency <n>', 'Max concurrent validations (defaults to env OPENSPEC_CONCURRENCY or 6)')
304
322
  .option('--no-interactive', 'Disable interactive prompts')
323
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
324
+ .addOption(hiddenStorePathOption())
305
325
  .action(async (itemName, options) => {
306
326
  try {
307
327
  const validateCommand = new ValidateCommand();
308
328
  await validateCommand.execute(itemName, options);
309
329
  }
310
330
  catch (error) {
311
- console.log();
312
- ora().fail(`Error: ${error.message}`);
331
+ failWithError(error, { enabled: options?.json, fallbackCode: 'validate_error' });
313
332
  process.exit(1);
314
333
  }
315
334
  });
@@ -327,6 +346,10 @@ program
327
346
  .option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
328
347
  .option('--no-scenarios', 'JSON only: Exclude scenario content')
329
348
  .option('-r, --requirement <id>', 'JSON only: Show specific requirement by ID (1-based)')
349
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
350
+ // Explicit registration required: allowUnknownOption would otherwise
351
+ // silently swallow --store-path instead of rejecting it deliberately.
352
+ .addOption(hiddenStorePathOption())
330
353
  // allow unknown options to pass-through to underlying command implementation
331
354
  .allowUnknownOption(true)
332
355
  .action(async (itemName, options) => {
@@ -335,8 +358,7 @@ program
335
358
  await showCommand.execute(itemName, options ?? {});
336
359
  }
337
360
  catch (error) {
338
- console.log();
339
- ora().fail(`Error: ${error.message}`);
361
+ failWithError(error, { enabled: options?.json, fallbackCode: 'show_error' });
340
362
  process.exit(1);
341
363
  }
342
364
  });
@@ -351,8 +373,7 @@ program
351
373
  await feedbackCommand.execute(message, options);
352
374
  }
353
375
  catch (error) {
354
- console.log();
355
- ora().fail(`Error: ${error.message}`);
376
+ failWithError(error);
356
377
  process.exit(1);
357
378
  }
358
379
  });
@@ -369,8 +390,7 @@ completionCmd
369
390
  await completionCommand.generate({ shell });
370
391
  }
371
392
  catch (error) {
372
- console.log();
373
- ora().fail(`Error: ${error.message}`);
393
+ failWithError(error);
374
394
  process.exit(1);
375
395
  }
376
396
  });
@@ -384,8 +404,7 @@ completionCmd
384
404
  await completionCommand.install({ shell, verbose: options?.verbose });
385
405
  }
386
406
  catch (error) {
387
- console.log();
388
- ora().fail(`Error: ${error.message}`);
407
+ failWithError(error);
389
408
  process.exit(1);
390
409
  }
391
410
  });
@@ -399,8 +418,7 @@ completionCmd
399
418
  await completionCommand.uninstall({ shell, yes: options?.yes });
400
419
  }
401
420
  catch (error) {
402
- console.log();
403
- ora().fail(`Error: ${error.message}`);
421
+ failWithError(error);
404
422
  process.exit(1);
405
423
  }
406
424
  });
@@ -428,13 +446,14 @@ program
428
446
  .option('--change <id>', 'Change name to show status for')
429
447
  .option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
430
448
  .option('--json', 'Output as JSON')
449
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
450
+ .addOption(hiddenStorePathOption())
431
451
  .action(async (options) => {
432
452
  try {
433
453
  await statusCommand(options);
434
454
  }
435
455
  catch (error) {
436
- console.log();
437
- ora().fail(`Error: ${error.message}`);
456
+ failWithError(error, { enabled: options.json, fallbackCode: 'change_error' });
438
457
  process.exit(1);
439
458
  }
440
459
  });
@@ -445,6 +464,8 @@ program
445
464
  .option('--change <id>', 'Change name')
446
465
  .option('--schema <name>', 'Schema override (auto-detected from config.yaml)')
447
466
  .option('--json', 'Output as JSON')
467
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
468
+ .addOption(hiddenStorePathOption())
448
469
  .action(async (artifactId, options) => {
449
470
  try {
450
471
  // Special case: "apply" is not an artifact, but a command to get apply instructions
@@ -456,8 +477,7 @@ program
456
477
  }
457
478
  }
458
479
  catch (error) {
459
- console.log();
460
- ora().fail(`Error: ${error.message}`);
480
+ failWithError(error, { enabled: options.json, fallbackCode: 'change_error' });
461
481
  process.exit(1);
462
482
  }
463
483
  });
@@ -472,8 +492,7 @@ program
472
492
  await templatesCommand(options);
473
493
  }
474
494
  catch (error) {
475
- console.log();
476
- ora().fail(`Error: ${error.message}`);
495
+ failWithError(error);
477
496
  process.exit(1);
478
497
  }
479
498
  });
@@ -487,8 +506,7 @@ program
487
506
  await schemasCommand(options);
488
507
  }
489
508
  catch (error) {
490
- console.log();
491
- ora().fail(`Error: ${error.message}`);
509
+ failWithError(error);
492
510
  process.exit(1);
493
511
  }
494
512
  });
@@ -498,39 +516,21 @@ newCmd
498
516
  .command('change <name>')
499
517
  .description('Create a new change directory')
500
518
  .option('--description <text>', 'Description to add to README.md')
501
- .option('--goal <text>', 'Workspace product goal to store with the change')
502
- .option('--areas <names>', 'Comma-separated affected workspace link names')
503
- .option('--initiative <id>', 'Link the repo-local change to an initiative')
504
- .option('--store <id>', 'Context store id for --initiative')
505
- .option('--store-path <path>', 'Existing local context store root for --initiative')
519
+ .option('--goal <text>', 'Optional goal metadata to store with the change')
506
520
  .option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`)
507
521
  .option('--json', 'Output as JSON')
522
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
523
+ .addOption(hiddenStorePathOption())
524
+ // Removed options kept registered (hidden) so users get a deliberate
525
+ // explanation instead of a generic unknown-option error.
526
+ .addOption(new Option('--initiative <id>', 'No longer supported').hideHelp())
527
+ .addOption(new Option('--areas <names>', 'No longer supported').hideHelp())
508
528
  .action(async (name, options) => {
509
529
  try {
510
530
  await newChangeCommand(name, options);
511
531
  }
512
532
  catch (error) {
513
- console.log();
514
- ora().fail(`Error: ${error.message}`);
515
- process.exit(1);
516
- }
517
- });
518
- // Set command group
519
- const setCmd = program.command('set').description('Set checked-in OpenSpec metadata');
520
- setCmd
521
- .command('change <name>')
522
- .description('Set repo-local change metadata')
523
- .option('--initiative <id>', 'Link the repo-local change to an initiative')
524
- .option('--store <id>', 'Context store id for --initiative')
525
- .option('--store-path <path>', 'Existing local context store root for --initiative')
526
- .option('--json', 'Output as JSON')
527
- .action(async (name, options) => {
528
- try {
529
- await setChangeCommand(name, options);
530
- }
531
- catch (error) {
532
- console.log();
533
- ora().fail(`Error: ${error.message}`);
533
+ failWithError(error);
534
534
  process.exit(1);
535
535
  }
536
536
  });
@@ -1,6 +1,9 @@
1
+ import type { RootOutput } from '../core/root-selection.js';
1
2
  export declare class ChangeCommand {
2
3
  private converter;
3
- constructor();
4
+ private rootPath?;
5
+ constructor(rootPath?: string);
6
+ private getChangesPath;
4
7
  /**
5
8
  * Show a change proposal.
6
9
  * - Text mode: raw markdown passthrough (no filters)
@@ -12,6 +15,7 @@ export declare class ChangeCommand {
12
15
  requirementsOnly?: boolean;
13
16
  deltasOnly?: boolean;
14
17
  noInteractive?: boolean;
18
+ rootOutput?: RootOutput;
15
19
  }): Promise<void>;
16
20
  /**
17
21
  * List active changes.
@@ -11,8 +11,15 @@ const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
11
11
  const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
12
12
  export class ChangeCommand {
13
13
  converter;
14
- constructor() {
14
+ rootPath;
15
+ // rootPath is set only by root-aware callers (top-level `show`); the
16
+ // deprecated noun-form commands stay cwd-based.
17
+ constructor(rootPath) {
15
18
  this.converter = new JsonConverter();
19
+ this.rootPath = rootPath;
20
+ }
21
+ getChangesPath() {
22
+ return path.join(this.rootPath ?? process.cwd(), 'openspec', 'changes');
16
23
  }
17
24
  /**
18
25
  * Show a change proposal.
@@ -21,7 +28,7 @@ export class ChangeCommand {
21
28
  * Note: --requirements-only is deprecated alias for --deltas-only
22
29
  */
23
30
  async show(changeName, options) {
24
- const changesPath = path.join(process.cwd(), 'openspec', 'changes');
31
+ const changesPath = this.getChangesPath();
25
32
  if (!changeName) {
26
33
  const canPrompt = isInteractive(options);
27
34
  const changes = await this.getActiveChanges(changesPath);
@@ -62,19 +69,14 @@ export class ChangeCommand {
62
69
  const title = this.extractTitle(contentForTitle, changeName);
63
70
  const id = parsed.name;
64
71
  const deltas = parsed.deltas || [];
65
- if (options.requirementsOnly || options.deltasOnly) {
66
- const output = { id, title, deltaCount: deltas.length, deltas };
67
- console.log(JSON.stringify(output, null, 2));
68
- }
69
- else {
70
- const output = {
71
- id,
72
- title,
73
- deltaCount: deltas.length,
74
- deltas,
75
- };
76
- console.log(JSON.stringify(output, null, 2));
77
- }
72
+ const output = {
73
+ id,
74
+ title,
75
+ deltaCount: deltas.length,
76
+ deltas,
77
+ ...(options.rootOutput ? { root: options.rootOutput } : {}),
78
+ };
79
+ console.log(JSON.stringify(output, null, 2));
78
80
  }
79
81
  else {
80
82
  const content = await fs.readFile(proposalPath, 'utf-8');
@@ -6,7 +6,7 @@ import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatV
6
6
  import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
7
7
  import { OPENSPEC_DIR_NAME } from '../core/config.js';
8
8
  import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
9
- import { findWorkspaceRoot, hasWorkspaceSkillProfileDrift, readOptionalWorkspaceViewState, } from '../core/workspace/index.js';
9
+ import { isPromptCancellationError } from './shared-output.js';
10
10
  const WORKFLOW_PROMPT_META = {
11
11
  propose: {
12
12
  name: 'Propose change',
@@ -53,10 +53,6 @@ const WORKFLOW_PROMPT_META = {
53
53
  description: 'Guided onboarding flow for OpenSpec',
54
54
  },
55
55
  };
56
- function isPromptCancellationError(error) {
57
- return (error instanceof Error &&
58
- (error.name === 'ExitPromptError' || error.message.includes('force closed the prompt with SIGINT')));
59
- }
60
56
  /**
61
57
  * Resolve the effective current profile state from global config defaults.
62
58
  */
@@ -133,16 +129,6 @@ export function diffProfileState(before, after) {
133
129
  lines,
134
130
  };
135
131
  }
136
- async function resolveWorkspaceConfigProfileContext(cwd = process.cwd()) {
137
- const workspaceRoot = await findWorkspaceRoot(cwd);
138
- if (!workspaceRoot) {
139
- return null;
140
- }
141
- return {
142
- root: workspaceRoot,
143
- commandCwd: cwd,
144
- };
145
- }
146
132
  function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
147
133
  const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
148
134
  if (!fs.existsSync(openspecDir)) {
@@ -153,28 +139,7 @@ function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
153
139
  }
154
140
  console.log(colorize('Warning: Global config is not applied to this project. Run `openspec update` to sync.'));
155
141
  }
156
- async function maybeWarnConfigDrift(state, colorize) {
157
- const workspaceContext = await resolveWorkspaceConfigProfileContext();
158
- if (workspaceContext) {
159
- let viewState = null;
160
- try {
161
- viewState = await readOptionalWorkspaceViewState(workspaceContext.root);
162
- }
163
- catch {
164
- return;
165
- }
166
- if (hasWorkspaceSkillProfileDrift(viewState)) {
167
- console.log(colorize('Warning: Workspace-local agent skills are out of sync with the active global profile. Run `openspec workspace update` to sync.'));
168
- }
169
- return;
170
- }
171
- maybeWarnProjectConfigDrift(process.cwd(), state, colorize);
172
- }
173
- function printConfigProfileApplyGuidance(workspaceContext) {
174
- if (workspaceContext) {
175
- console.log('Config updated. Run `openspec workspace update` to apply it to workspace-local skills.');
176
- return;
177
- }
142
+ function printConfigProfileApplyGuidance() {
178
143
  console.log('Config updated. Run `openspec update` in your projects to apply.');
179
144
  }
180
145
  /**
@@ -418,8 +383,7 @@ export function registerConfigCommand(program) {
418
383
  config.workflows = [...CORE_WORKFLOWS];
419
384
  // Preserve delivery setting
420
385
  saveGlobalConfig(config);
421
- const workspaceContext = await resolveWorkspaceConfigProfileContext();
422
- printConfigProfileApplyGuidance(workspaceContext);
386
+ printConfigProfileApplyGuidance();
423
387
  return;
424
388
  }
425
389
  if (preset) {
@@ -472,7 +436,7 @@ export function registerConfigCommand(program) {
472
436
  });
473
437
  if (action === 'keep') {
474
438
  console.log('No config changes.');
475
- await maybeWarnConfigDrift(currentState, chalk.yellow);
439
+ maybeWarnProjectConfigDrift(process.cwd(), currentState, chalk.yellow);
476
440
  return;
477
441
  }
478
442
  const nextState = {
@@ -541,7 +505,7 @@ export function registerConfigCommand(program) {
541
505
  const diff = diffProfileState(currentState, nextState);
542
506
  if (!diff.hasChanges) {
543
507
  console.log('No config changes.');
544
- await maybeWarnConfigDrift(nextState, chalk.yellow);
508
+ maybeWarnProjectConfigDrift(process.cwd(), nextState, chalk.yellow);
545
509
  return;
546
510
  }
547
511
  console.log(chalk.bold('\nConfig changes:'));
@@ -553,29 +517,6 @@ export function registerConfigCommand(program) {
553
517
  config.delivery = nextState.delivery;
554
518
  config.workflows = nextState.workflows;
555
519
  saveGlobalConfig(config);
556
- const workspaceContext = await resolveWorkspaceConfigProfileContext();
557
- if (workspaceContext) {
558
- const applyNow = await confirm({
559
- message: 'Apply changes to this workspace now?',
560
- default: true,
561
- });
562
- if (applyNow) {
563
- try {
564
- execSync('npx openspec workspace update', {
565
- stdio: 'inherit',
566
- cwd: workspaceContext.commandCwd,
567
- });
568
- console.log('Run `openspec workspace update` in your other workspaces to apply.');
569
- }
570
- catch {
571
- console.error('`openspec workspace update` failed. Please run it manually to apply the profile changes.');
572
- process.exitCode = 1;
573
- }
574
- return;
575
- }
576
- printConfigProfileApplyGuidance(workspaceContext);
577
- return;
578
- }
579
520
  // Check if inside an OpenSpec project
580
521
  const projectDir = process.cwd();
581
522
  const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
@@ -596,7 +537,7 @@ export function registerConfigCommand(program) {
596
537
  return;
597
538
  }
598
539
  }
599
- printConfigProfileApplyGuidance(null);
540
+ printConfigProfileApplyGuidance();
600
541
  }
601
542
  catch (error) {
602
543
  if (isPromptCancellationError(error)) {
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerContextCommand(program: Command): void;
3
+ //# sourceMappingURL=context.d.ts.map