@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
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
@@ -77,6 +85,18 @@ AI: Archived to openspec/changes/archive/2025-01-23-add-dark-mode/
77
85
 
78
86
  </details>
79
87
 
88
+ ## Why teams adopt OpenSpec
89
+
90
+ Solo, OpenSpec keeps you and your AI honest on a single repo. On a team, the hard part moves: a feature spans the API server, the web app, and a shared library; requirements are owned by one team and consumed by others; planning starts before any code exists.
91
+
92
+ **[Stores](docs/stores-beta/user-guide.md)** are the answer — planning in a repo of its own. The same `openspec/` shape you already know (specs and changes), shared by `git push` like anything else. One source of truth your whole team and every coding agent can read, across every repo.
93
+
94
+ - **Cross-repo features** — one change, one plan, even when the code lands in three repos.
95
+ - **Shared requirements** — a platform team owns the specs; product teams reference them read-only, right where their coding agent can read them. No drifting wiki.
96
+ - **Plan before code** — capture the plan in the store now; the code repos catch up later.
97
+
98
+ > Stores are in **beta**. Start with the [Stores User Guide](docs/stores-beta/user-guide.md).
99
+
80
100
  ## Quick Start
81
101
 
82
102
  **Requires Node.js 20.19.0 or higher.**
@@ -94,9 +114,12 @@ cd your-project
94
114
  openspec init
95
115
  ```
96
116
 
97
- Now tell your AI: `/opsx:propose <what-you-want-to-build>`
117
+ Now talk to your AI:
98
118
 
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`.
119
+ - **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))
120
+ - **Already know what you want?** Go straight to `/opsx:propose <what-you-want-to-build>`.
121
+
122
+ 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
123
 
101
124
  > [!NOTE]
102
125
  > Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
@@ -105,14 +128,24 @@ If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/
105
128
 
106
129
  ## Docs
107
130
 
131
+ **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`).
132
+
108
133
  → **[Getting Started](docs/getting-started.md)**: first steps<br>
134
+ → **[Explore First](docs/explore.md)**: think it through with `/opsx:explore` before you commit<br>
135
+ → **[How Commands Work](docs/how-commands-work.md)**: where slash commands run vs the CLI<br>
136
+ → **[Core Concepts at a Glance](docs/overview.md)**: the whole mental model, one page<br>
137
+ → **[Examples & Recipes](docs/examples.md)**: real changes, start to finish<br>
109
138
  → **[Workflows](docs/workflows.md)**: combos and patterns<br>
139
+ → **[Existing Projects](docs/existing-projects.md)**: adopt OpenSpec on a brownfield codebase<br>
140
+ → **[Editing a Change](docs/editing-changes.md)**: update artifacts, go back, reconcile manual edits<br>
110
141
  → **[Commands](docs/commands.md)**: slash commands & skills<br>
111
142
  → **[CLI](docs/cli.md)**: terminal reference<br>
143
+ → **[Stores](docs/stores-beta/user-guide.md)**: plan in a separate repo, shared across your team (beta)<br>
112
144
  → **[Supported Tools](docs/supported-tools.md)**: tool integrations & install paths<br>
113
145
  → **[Concepts](docs/concepts.md)**: how it all fits<br>
114
146
  → **[Multi-Language](docs/multi-language.md)**: multi-language support<br>
115
- → **[Customization](docs/customization.md)**: make it yours
147
+ → **[Customization](docs/customization.md)**: make it yours<br>
148
+ → **[FAQ](docs/faq.md)** · **[Troubleshooting](docs/troubleshooting.md)** · **[Glossary](docs/glossary.md)**: quick help
116
149
 
117
150
 
118
151
  ## Community schemas
@@ -129,7 +162,7 @@ AI coding assistants are powerful but unpredictable when requirements live only
129
162
  - **Agree before you build** — human and AI align on specs before code gets written
130
163
  - **Stay organized** — each change gets its own folder with proposal, specs, design, and tasks
131
164
  - **Work fluidly** — update any artifact anytime, no rigid phase gates
132
- - **Use your tools** — works with 20+ AI assistants via slash commands
165
+ - **Use your tools** — works with 30+ AI assistants via slash commands
133
166
 
134
167
  ### How we compare
135
168
 
@@ -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.
@@ -29,7 +33,6 @@ export declare class ChangeCommand {
29
33
  }): Promise<void>;
30
34
  private getActiveChanges;
31
35
  private extractTitle;
32
- private countTasks;
33
36
  private printNextSteps;
34
37
  }
35
38
  //# sourceMappingURL=change.d.ts.map
@@ -5,14 +5,20 @@ import { Validator } from '../core/validation/validator.js';
5
5
  import { ChangeParser } from '../core/parsers/change-parser.js';
6
6
  import { isInteractive } from '../utils/interactive.js';
7
7
  import { getActiveChangeIds } from '../utils/item-discovery.js';
8
+ import { getTaskProgressForChange } from '../utils/task-progress.js';
8
9
  // Constants for better maintainability
9
10
  const ARCHIVE_DIR = 'archive';
10
- const TASK_PATTERN = /^[-*]\s+\[[\sx]\]/i;
11
- const COMPLETED_TASK_PATTERN = /^[-*]\s+\[x\]/i;
12
11
  export class ChangeCommand {
13
12
  converter;
14
- constructor() {
13
+ rootPath;
14
+ // rootPath is set only by root-aware callers (top-level `show`); the
15
+ // deprecated noun-form commands stay cwd-based.
16
+ constructor(rootPath) {
15
17
  this.converter = new JsonConverter();
18
+ this.rootPath = rootPath;
19
+ }
20
+ getChangesPath() {
21
+ return path.join(this.rootPath ?? process.cwd(), 'openspec', 'changes');
16
22
  }
17
23
  /**
18
24
  * Show a change proposal.
@@ -21,7 +27,7 @@ export class ChangeCommand {
21
27
  * Note: --requirements-only is deprecated alias for --deltas-only
22
28
  */
23
29
  async show(changeName, options) {
24
- const changesPath = path.join(process.cwd(), 'openspec', 'changes');
30
+ const changesPath = this.getChangesPath();
25
31
  if (!changeName) {
26
32
  const canPrompt = isInteractive(options);
27
33
  const changes = await this.getActiveChanges(changesPath);
@@ -62,19 +68,14 @@ export class ChangeCommand {
62
68
  const title = this.extractTitle(contentForTitle, changeName);
63
69
  const id = parsed.name;
64
70
  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
- }
71
+ const output = {
72
+ id,
73
+ title,
74
+ deltaCount: deltas.length,
75
+ deltas,
76
+ ...(options.rootOutput ? { root: options.rootOutput } : {}),
77
+ };
78
+ console.log(JSON.stringify(output, null, 2));
78
79
  }
79
80
  else {
80
81
  const content = await fs.readFile(proposalPath, 'utf-8');
@@ -92,23 +93,14 @@ export class ChangeCommand {
92
93
  if (options?.json) {
93
94
  const changeDetails = await Promise.all(changes.map(async (changeName) => {
94
95
  const proposalPath = path.join(changesPath, changeName, 'proposal.md');
95
- const tasksPath = path.join(changesPath, changeName, 'tasks.md');
96
96
  try {
97
97
  const content = await fs.readFile(proposalPath, 'utf-8');
98
98
  const changeDir = path.join(changesPath, changeName);
99
99
  const parser = new ChangeParser(content, changeDir);
100
100
  const change = await parser.parseChangeWithDeltas(changeName);
101
- let taskStatus = { total: 0, completed: 0 };
102
- try {
103
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
104
- taskStatus = this.countTasks(tasksContent);
105
- }
106
- catch (error) {
107
- // Tasks file may not exist, which is okay
108
- if (process.env.DEBUG) {
109
- console.error(`Failed to read tasks file at ${tasksPath}:`, error);
110
- }
111
- }
101
+ // Resolve task progress through the shared tracked-tasks helper so
102
+ // this deprecated noun-form list cannot re-fork the resolution (#1202).
103
+ const taskStatus = await getTaskProgressForChange(changesPath, changeName, process.cwd());
112
104
  return {
113
105
  id: changeName,
114
106
  title: this.extractTitle(content, changeName),
@@ -142,21 +134,11 @@ export class ChangeCommand {
142
134
  // Long format: id: title and minimal counts
143
135
  for (const changeName of sorted) {
144
136
  const proposalPath = path.join(changesPath, changeName, 'proposal.md');
145
- const tasksPath = path.join(changesPath, changeName, 'tasks.md');
146
137
  try {
147
138
  const content = await fs.readFile(proposalPath, 'utf-8');
148
139
  const title = this.extractTitle(content, changeName);
149
- let taskStatusText = '';
150
- try {
151
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
152
- const { total, completed } = this.countTasks(tasksContent);
153
- taskStatusText = ` [tasks ${completed}/${total}]`;
154
- }
155
- catch (error) {
156
- if (process.env.DEBUG) {
157
- console.error(`Failed to read tasks file at ${tasksPath}:`, error);
158
- }
159
- }
140
+ const { total, completed } = await getTaskProgressForChange(changesPath, changeName, process.cwd());
141
+ const taskStatusText = total > 0 ? ` [tasks ${completed}/${total}]` : '';
160
142
  const changeDir = path.join(changesPath, changeName);
161
143
  const parser = new ChangeParser(await fs.readFile(proposalPath, 'utf-8'), changeDir);
162
144
  const change = await parser.parseChangeWithDeltas(changeName);
@@ -251,20 +233,6 @@ export class ChangeCommand {
251
233
  const match = content.match(/^#\s+(?:Change:\s+)?(.+)$/im);
252
234
  return match ? match[1].trim() : changeName;
253
235
  }
254
- countTasks(content) {
255
- const lines = content.split('\n');
256
- let total = 0;
257
- let completed = 0;
258
- for (const line of lines) {
259
- if (line.match(TASK_PATTERN)) {
260
- total++;
261
- if (line.match(COMPLETED_TASK_PATTERN)) {
262
- completed++;
263
- }
264
- }
265
- }
266
- return { total, completed };
267
- }
268
236
  printNextSteps() {
269
237
  const bullets = [];
270
238
  bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');