@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
@@ -1,62 +1,37 @@
1
1
  /**
2
2
  * New Change Command
3
3
  *
4
- * Creates a new change directory with optional description and schema.
4
+ * Creates a new change directory with optional description and schema in the
5
+ * resolved OpenSpec root. `--store <id>` selects a registered store's
6
+ * root; initiative linking and workspace affected areas are no longer part of
7
+ * this command.
5
8
  */
6
9
  import ora from 'ora';
7
10
  import path from 'path';
8
11
  import { createChange, validateChangeName } from '../../utils/change-utils.js';
9
- import { formatChangeLocation, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
10
- import { validateSchemaExists } from './shared.js';
11
- import { resolveInitiativeLinkReference, } from '../../core/collections/initiatives/index.js';
12
- import { assertInitiativeSelectorsHaveReference, assertRepoLocalInitiativeLinkPlanningHome, formatInitiativeLink, printJson, statusFromError, } from './initiative-link.js';
12
+ import { formatChangeLocation } from '../../core/planning-home.js';
13
+ import { resolveRootForCommand, RootSelectionError, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
14
+ import { printJson, statusFromError, validateSchemaExists } from './shared.js';
13
15
  // -----------------------------------------------------------------------------
14
16
  // Command Implementation
15
17
  // -----------------------------------------------------------------------------
16
- function parseAffectedAreas(value) {
17
- return (value ?? '')
18
- .split(',')
19
- .map((area) => area.trim())
20
- .filter((area) => area.length > 0);
21
- }
22
- function validateWorkspaceAffectedAreas(planningHome, affectedAreas) {
23
- if (affectedAreas.length === 0) {
24
- return;
25
- }
26
- if (planningHome.kind !== 'workspace') {
27
- throw new Error('--areas can only be used when creating a workspace-scoped change');
18
+ function assertRemovedOptionsAbsent(options) {
19
+ if (options.initiative !== undefined) {
20
+ throw new RootSelectionError('--initiative is no longer supported. Normal changes no longer attach to initiatives; --store <id> selects the OpenSpec root.', 'initiative_option_removed', { target: 'change.options' });
28
21
  }
29
- const validAreas = new Set(planningHome.workspace?.links ?? []);
30
- const invalidAreas = affectedAreas.filter((area) => !validAreas.has(area));
31
- if (invalidAreas.length > 0) {
32
- const validList = [...validAreas].sort((a, b) => a.localeCompare(b));
33
- const validMessage = validList.length > 0 ? validList.join(', ') : '(no registered links)';
34
- throw new Error(`Invalid affected area${invalidAreas.length === 1 ? '' : 's'}: ${invalidAreas.join(', ')}. ` +
35
- `Valid workspace link names: ${validMessage}`);
22
+ if (options.areas !== undefined) {
23
+ throw new RootSelectionError('--areas is no longer supported. Workspace affected areas are not part of the normal OpenSpec root path.', 'areas_option_removed', { target: 'change.options' });
36
24
  }
37
25
  }
38
- function outputForCreatedChange(id, changeDir, schema, initiative) {
39
- return {
40
- change: {
41
- id,
42
- path: changeDir,
43
- metadataPath: path.join(changeDir, '.openspec.yaml'),
44
- schema,
45
- },
46
- ...(initiative ? { initiative } : {}),
47
- };
48
- }
49
- function printCreatedChangeHuman(payload, planningHome) {
50
- if (!payload.change) {
51
- return;
52
- }
53
- const location = formatChangeLocation(planningHome, payload.change.id);
54
- const scope = planningHome.kind === 'workspace' ? 'workspace change' : 'change';
55
- console.log(`Created ${scope} '${payload.change.id}' at ${location}/`);
26
+ function printCreatedChangeHuman(payload, root) {
27
+ // A relative path is only honest when the root is where the user
28
+ // stands; a distant ancestor root gets the absolute path.
29
+ const location = !isStoreSelectedRoot(root) && root.path === process.cwd()
30
+ ? formatChangeLocation(toPlanningHome(root), payload.change.id)
31
+ : payload.change.path;
32
+ console.log(`Created change '${payload.change.id}' at ${location}/`);
56
33
  console.log(`Schema: ${payload.change.schema}`);
57
- if (payload.initiative) {
58
- console.log(`Initiative: ${formatInitiativeLink(payload.initiative)}`);
59
- }
34
+ console.log(`Next: ${withStoreFlag(root, `openspec status --change ${payload.change.id}`)}`);
60
35
  }
61
36
  export async function newChangeCommand(name, options) {
62
37
  const spinner = options.json ? undefined : ora();
@@ -68,38 +43,29 @@ export async function newChangeCommand(name, options) {
68
43
  if (!validation.valid) {
69
44
  throw new Error(validation.error);
70
45
  }
71
- assertInitiativeSelectorsHaveReference(options);
72
- const planningHome = resolveCurrentPlanningHomeSync();
73
- const projectRoot = planningHome.root;
74
- const affectedAreas = parseAffectedAreas(options.areas);
75
- validateWorkspaceAffectedAreas(planningHome, affectedAreas);
76
- let initiative;
77
- if (options.initiative !== undefined) {
78
- assertRepoLocalInitiativeLinkPlanningHome(planningHome);
79
- initiative = await resolveInitiativeLinkReference(options.initiative, {
80
- store: options.store,
81
- storePath: options.storePath,
82
- });
46
+ assertRemovedOptionsAbsent(options);
47
+ const root = await resolveRootForCommand(options, {
48
+ json: options.json,
49
+ failurePayload: { change: null },
50
+ });
51
+ if (!root) {
52
+ return;
83
53
  }
54
+ const projectRoot = root.path;
84
55
  // Validate schema if provided
85
56
  if (options.schema) {
86
57
  validateSchemaExists(options.schema, projectRoot);
87
58
  }
88
- const resolvedSchema = options.schema ?? planningHome.defaultSchema;
59
+ const resolvedSchema = options.schema ?? root.defaultSchema;
89
60
  if (spinner) {
90
61
  spinner.start(`Creating change '${name}' with schema '${resolvedSchema}'...`);
91
62
  }
92
- const workspaceGoal = planningHome.kind === 'workspace'
93
- ? options.goal ?? options.description
94
- : options.goal;
95
63
  const result = await createChange(projectRoot, name, {
96
64
  schema: options.schema,
97
- defaultSchema: planningHome.defaultSchema,
98
- changesDir: planningHome.changesDir,
65
+ defaultSchema: root.defaultSchema,
66
+ changesDir: root.changesDir,
99
67
  metadata: {
100
- ...(workspaceGoal ? { goal: workspaceGoal } : {}),
101
- ...(affectedAreas.length > 0 ? { affected_areas: affectedAreas } : {}),
102
- ...(initiative ? { initiative } : {}),
68
+ ...(options.goal ? { goal: options.goal } : {}),
103
69
  },
104
70
  });
105
71
  // If description provided, create README.md with description
@@ -108,22 +74,21 @@ export async function newChangeCommand(name, options) {
108
74
  const readmePath = path.join(result.changeDir, 'README.md');
109
75
  await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8');
110
76
  }
111
- const payload = outputForCreatedChange(name, result.changeDir, result.schema, initiative);
77
+ const payload = {
78
+ change: {
79
+ id: name,
80
+ path: result.changeDir,
81
+ metadataPath: path.join(result.changeDir, '.openspec.yaml'),
82
+ schema: result.schema,
83
+ },
84
+ root: toRootOutput(root),
85
+ };
112
86
  if (options.json) {
113
87
  printJson(payload);
114
88
  return;
115
89
  }
116
90
  spinner?.stop();
117
- printCreatedChangeHuman(payload, planningHome);
118
- if (planningHome.kind === 'workspace' && !initiative) {
119
- if (affectedAreas.length > 0) {
120
- console.log(`Affected areas: ${affectedAreas.join(', ')}`);
121
- }
122
- else {
123
- console.log('Affected areas: unresolved; identify them in change metadata or coordination tasks as planning continues.');
124
- }
125
- console.log('Next: run openspec status --change "' + name + '" to inspect workspace planning artifacts.');
126
- }
91
+ printCreatedChangeHuman(payload, root);
127
92
  }
128
93
  catch (error) {
129
94
  spinner?.stop();
@@ -4,7 +4,14 @@
4
4
  * This module contains types, constants, and validation helpers used across
5
5
  * multiple artifact workflow commands.
6
6
  */
7
- import type { InitiativeLink } from '../../core/change-metadata/index.js';
7
+ import type { ReferenceIndexEntry } from '../../core/references.js';
8
+ export interface ChangeCommandStatus {
9
+ severity: 'error' | 'warning';
10
+ code: string;
11
+ message: string;
12
+ target?: string;
13
+ fix?: string;
14
+ }
8
15
  export interface TaskItem {
9
16
  id: string;
10
17
  description: string;
@@ -14,7 +21,6 @@ export interface ApplyInstructions {
14
21
  changeName: string;
15
22
  changeDir: string;
16
23
  schemaName: string;
17
- initiative?: InitiativeLink;
18
24
  contextFiles: Record<string, string[]>;
19
25
  progress: {
20
26
  total: number;
@@ -25,8 +31,12 @@ export interface ApplyInstructions {
25
31
  state: 'blocked' | 'all_done' | 'ready';
26
32
  missingArtifacts?: string[];
27
33
  instruction: string;
34
+ /** Referenced-store index (read-only upstream context; omitted when none declared) */
35
+ references?: ReferenceIndexEntry[];
28
36
  }
29
37
  export declare const DEFAULT_SCHEMA = "spec-driven";
38
+ export declare function printJson(payload: unknown): void;
39
+ export declare function statusFromError(error: unknown): ChangeCommandStatus;
30
40
  /**
31
41
  * Checks if color output is disabled via NO_COLOR env or --no-color flag.
32
42
  */
@@ -48,7 +58,9 @@ export declare function getAvailableChanges(projectRoot: string, changesDir?: st
48
58
  * Validates that a change exists and returns available changes if not.
49
59
  * Checks directory existence directly to support scaffolded changes (without proposal.md).
50
60
  */
51
- export declare function validateChangeExists(changeName: string | undefined, projectRoot: string, changesDir?: string): Promise<string>;
61
+ export declare function validateChangeExists(changeName: string | undefined, projectRoot: string, changesDir?: string, hints?: {
62
+ newChangeHint?: string;
63
+ }): Promise<string>;
52
64
  /**
53
65
  * Validates that a schema exists and returns available schemas if not.
54
66
  *
@@ -8,6 +8,7 @@ import chalk from 'chalk';
8
8
  import path from 'path';
9
9
  import * as fs from 'fs';
10
10
  import { getSchemaDir, listSchemas } from '../../core/artifact-graph/index.js';
11
+ import { isRootSelectionError } from '../../core/root-selection.js';
11
12
  import { validateChangeName } from '../../utils/change-utils.js';
12
13
  // -----------------------------------------------------------------------------
13
14
  // Constants
@@ -16,6 +17,19 @@ export const DEFAULT_SCHEMA = 'spec-driven';
16
17
  // -----------------------------------------------------------------------------
17
18
  // Utility Functions
18
19
  // -----------------------------------------------------------------------------
20
+ export function printJson(payload) {
21
+ console.log(JSON.stringify(payload, null, 2));
22
+ }
23
+ export function statusFromError(error) {
24
+ if (isRootSelectionError(error)) {
25
+ return { ...error.diagnostic };
26
+ }
27
+ return {
28
+ severity: 'error',
29
+ code: 'change_error',
30
+ message: error instanceof Error ? error.message : String(error),
31
+ };
32
+ }
19
33
  /**
20
34
  * Checks if color output is disabled via NO_COLOR env or --no-color flag.
21
35
  */
@@ -74,11 +88,14 @@ export async function getAvailableChanges(projectRoot, changesDir = path.join(pr
74
88
  * Validates that a change exists and returns available changes if not.
75
89
  * Checks directory existence directly to support scaffolded changes (without proposal.md).
76
90
  */
77
- export async function validateChangeExists(changeName, projectRoot, changesDir = path.join(projectRoot, 'openspec', 'changes')) {
91
+ export async function validateChangeExists(changeName, projectRoot, changesDir = path.join(projectRoot, 'openspec', 'changes'), hints = {}) {
92
+ // Hints must stay pasteable: callers with a selected store pass a
93
+ // store-carrying hint so following it lands in the same root.
94
+ const newChangeHint = hints.newChangeHint ?? 'openspec new change <name>';
78
95
  if (!changeName) {
79
96
  const available = await getAvailableChanges(projectRoot, changesDir);
80
97
  if (available.length === 0) {
81
- throw new Error('No changes found. Create one with: openspec new change <name>');
98
+ throw new Error(`No changes found. Create one with: ${newChangeHint}`);
82
99
  }
83
100
  throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
84
101
  }
@@ -93,7 +110,7 @@ export async function validateChangeExists(changeName, projectRoot, changesDir =
93
110
  if (!exists) {
94
111
  const available = await getAvailableChanges(projectRoot, changesDir);
95
112
  if (available.length === 0) {
96
- throw new Error(`Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>`);
113
+ throw new Error(`Change '${changeName}' not found. No changes exist. Create one with: ${newChangeHint}`);
97
114
  }
98
115
  throw new Error(`Change '${changeName}' not found. Available changes:\n ${available.join('\n ')}`);
99
116
  }
@@ -7,6 +7,8 @@ import { type ChangeStatus } from '../../core/artifact-graph/index.js';
7
7
  export interface StatusOptions {
8
8
  change?: string;
9
9
  schema?: string;
10
+ store?: string;
11
+ storePath?: string;
10
12
  json?: boolean;
11
13
  }
12
14
  export declare function statusCommand(options: StatusOptions): Promise<void>;
@@ -5,35 +5,44 @@
5
5
  */
6
6
  import ora from 'ora';
7
7
  import chalk from 'chalk';
8
- import { resolveCurrentPlanningHomeSync, getChangeDir } from '../../core/planning-home.js';
8
+ import { getChangeDir } from '../../core/planning-home.js';
9
+ import { resolveRootForCommand, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
9
10
  import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
10
11
  import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
11
12
  // -----------------------------------------------------------------------------
12
13
  // Command Implementation
13
14
  // -----------------------------------------------------------------------------
14
15
  export async function statusCommand(options) {
16
+ // The root resolves (and the store banner prints) before the spinner starts
17
+ // so the two do not fight over stderr.
18
+ const root = await resolveRootForCommand(options, { json: options.json });
19
+ if (!root) {
20
+ return;
21
+ }
15
22
  const spinner = options.json ? undefined : ora('Loading change status...').start();
16
23
  try {
17
- const planningHome = resolveCurrentPlanningHomeSync();
18
- const projectRoot = planningHome.root;
24
+ const planningHome = toPlanningHome(root);
25
+ const projectRoot = root.path;
26
+ const rootOutput = toRootOutput(root);
27
+ const newChangeHint = withStoreFlag(root, 'openspec new change <name>');
19
28
  // Handle no-changes case gracefully — status is informational,
20
29
  // so "no changes" is a valid state, not an error.
21
30
  if (!options.change) {
22
- const available = await getAvailableChanges(projectRoot, planningHome.changesDir);
31
+ const available = await getAvailableChanges(projectRoot, root.changesDir);
23
32
  if (available.length === 0) {
24
33
  spinner?.stop();
25
34
  if (options.json) {
26
- console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
35
+ console.log(JSON.stringify({ changes: [], message: 'No active changes.', root: rootOutput }, null, 2));
27
36
  return;
28
37
  }
29
- console.log('No active changes. Create one with: openspec new change <name>');
38
+ console.log(`No active changes. Create one with: ${newChangeHint}`);
30
39
  return;
31
40
  }
32
41
  // Changes exist but --change not provided
33
42
  spinner?.stop();
34
43
  throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
35
44
  }
36
- const changeName = await validateChangeExists(options.change, projectRoot, planningHome.changesDir);
45
+ const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint });
37
46
  // Validate schema if explicitly provided
38
47
  if (options.schema) {
39
48
  validateSchemaExists(options.schema, projectRoot);
@@ -43,10 +52,10 @@ export async function statusCommand(options) {
43
52
  changeDir: getChangeDir(planningHome, changeName),
44
53
  planningHome,
45
54
  });
46
- const status = formatChangeStatus(context);
55
+ const status = formatChangeStatus(context, isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
47
56
  spinner?.stop();
48
57
  if (options.json) {
49
- console.log(JSON.stringify(status, null, 2));
58
+ console.log(JSON.stringify({ ...status, root: rootOutput }, null, 2));
50
59
  return;
51
60
  }
52
61
  printStatusText(status);
@@ -61,14 +70,7 @@ export function printStatusText(status) {
61
70
  const total = status.artifacts.length;
62
71
  console.log(`Change: ${status.changeName}`);
63
72
  console.log(`Schema: ${status.schemaName}`);
64
- if (status.initiative) {
65
- console.log(`Initiative: ${status.initiative.store}/${status.initiative.id}`);
66
- }
67
- if (status.planningHome) {
68
- const label = status.planningHome.kind === 'workspace'
69
- ? `workspace${status.planningHome.workspaceName ? ` (${status.planningHome.workspaceName})` : ''}`
70
- : 'repo';
71
- console.log(`Planning home: ${label}`);
73
+ if (status.changeRoot) {
72
74
  console.log(`Change root: ${status.changeRoot}`);
73
75
  }
74
76
  console.log(`Progress: ${doneCount}/${total} artifacts complete`);
@@ -0,0 +1,19 @@
1
+ import { type OpenerDefinition, type OpenerScanOptions } from '../core/openers.js';
2
+ import { StoreError } from '../core/store/errors.js';
3
+ import { type Workset, type WorksetMember } from '../core/worksets.js';
4
+ /** Concurrent stats; the first invalid flag (by flag order) reports. */
5
+ export declare function resolveMemberFlags(flags: string[]): Promise<WorksetMember[]>;
6
+ /** One spelling of "this tool id must exist in the merged table". */
7
+ export declare function assertKnownTool(tool: string, table: OpenerDefinition[]): void;
8
+ /** Final assembly shared by both compose paths: one validation rule. */
9
+ export declare function finalizeWorkset(name: string, members: WorksetMember[], tool: string | undefined, table: OpenerDefinition[]): Workset;
10
+ /** The aligned `<name> <path>` rows used by list, remove, and the
11
+ * open fallback; callers pick the stream and indent. */
12
+ export declare function formatMemberRows(members: WorksetMember[]): string[];
13
+ export declare function toolUnknownError(toolId: string, table: OpenerDefinition[]): StoreError;
14
+ /** Stops at the first installed alternative instead of scanning all. */
15
+ export declare function firstInstalledAlternative(table: OpenerDefinition[], excludeId: string | undefined, scan?: OpenerScanOptions): string | null;
16
+ export declare function toolUnavailableError(opener: OpenerDefinition, table: OpenerDefinition[], worksetName: string, scan?: OpenerScanOptions): StoreError;
17
+ /** Interactive open with no saved tool and nothing installed at all. */
18
+ export declare function noToolInstalledError(table: OpenerDefinition[], worksetName: string): StoreError;
19
+ //# sourceMappingURL=workset-input.d.ts.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Input resolution and error builders shared by the workset command
3
+ * and its interactive prompt flows.
4
+ */
5
+ import * as path from 'node:path';
6
+ import { pathIsDirectory } from '../core/file-state.js';
7
+ import { findOpener, isOpenerCommandAvailable, isOpenerEnabled, } from '../core/openers.js';
8
+ import { StoreError } from '../core/store/errors.js';
9
+ import { expandUserPath } from '../core/store/operations.js';
10
+ import { getGlobalConfigPath } from '../core/global-config.js';
11
+ import { memberLabelProblem, memberListProblem, } from '../core/worksets.js';
12
+ function memberInvalidError(problem) {
13
+ return new StoreError(`Invalid workset member: ${problem}.`, 'workset_member_invalid', {
14
+ target: 'workset.member',
15
+ fix: 'Pass --member <path> with an existing folder, or --member <name>=<path> to label it.',
16
+ });
17
+ }
18
+ /** `--member <path>` or `--member <name>=<path>` (the first `=` splits). */
19
+ async function resolveMemberFlag(raw) {
20
+ const separator = raw.indexOf('=');
21
+ const label = separator > 0 ? raw.slice(0, separator) : undefined;
22
+ const rawPath = separator > 0 ? raw.slice(separator + 1) : raw;
23
+ if (rawPath.length === 0) {
24
+ throw memberInvalidError(`'${raw}' has no path`);
25
+ }
26
+ const resolvedPath = path.resolve(expandUserPath(rawPath));
27
+ if (!(await pathIsDirectory(resolvedPath))) {
28
+ throw memberInvalidError(`'${rawPath}' is not an existing folder`);
29
+ }
30
+ const name = label ?? path.basename(resolvedPath);
31
+ const labelProblem = memberLabelProblem(name);
32
+ if (labelProblem !== null) {
33
+ throw memberInvalidError(labelProblem);
34
+ }
35
+ return { name, path: resolvedPath };
36
+ }
37
+ /** Concurrent stats; the first invalid flag (by flag order) reports. */
38
+ export async function resolveMemberFlags(flags) {
39
+ const settled = await Promise.allSettled(flags.map(resolveMemberFlag));
40
+ const members = [];
41
+ for (const result of settled) {
42
+ if (result.status === 'rejected') {
43
+ throw result.reason;
44
+ }
45
+ members.push(result.value);
46
+ }
47
+ return members;
48
+ }
49
+ /** One spelling of "this tool id must exist in the merged table". */
50
+ export function assertKnownTool(tool, table) {
51
+ if (findOpener(table, tool) === null) {
52
+ throw toolUnknownError(tool, table);
53
+ }
54
+ }
55
+ /** Final assembly shared by both compose paths: one validation rule. */
56
+ export function finalizeWorkset(name, members, tool, table) {
57
+ const problem = memberListProblem(members);
58
+ if (problem !== null) {
59
+ throw memberInvalidError(problem);
60
+ }
61
+ if (tool !== undefined) {
62
+ assertKnownTool(tool, table);
63
+ }
64
+ return {
65
+ name,
66
+ ...(tool !== undefined ? { tool } : {}),
67
+ members,
68
+ };
69
+ }
70
+ /** The aligned `<name> <path>` rows used by list, remove, and the
71
+ * open fallback; callers pick the stream and indent. */
72
+ export function formatMemberRows(members) {
73
+ const width = Math.max(...members.map((member) => member.name.length));
74
+ return members.map((member) => `${member.name.padEnd(width)} ${member.path}`);
75
+ }
76
+ export function toolUnknownError(toolId, table) {
77
+ const knownIds = table
78
+ .filter((opener) => isOpenerEnabled(opener))
79
+ .map((opener) => opener.id)
80
+ .join(', ');
81
+ return new StoreError(`Unknown tool '${toolId}'.`, 'workset_tool_unknown', {
82
+ target: 'workset.tool',
83
+ fix: `Known tools: ${knownIds}. Add new tools under "openers" in ${getGlobalConfigPath()}.`,
84
+ });
85
+ }
86
+ /** Stops at the first installed alternative instead of scanning all. */
87
+ export function firstInstalledAlternative(table, excludeId, scan) {
88
+ return (table.find((candidate) => candidate.id !== excludeId &&
89
+ isOpenerEnabled(candidate) &&
90
+ isOpenerCommandAvailable(candidate.command, scan))?.id ?? null);
91
+ }
92
+ export function toolUnavailableError(opener, table, worksetName, scan) {
93
+ const alternative = firstInstalledAlternative(table, opener.id, scan);
94
+ return new StoreError(`${opener.label} ('${opener.command}') is not on PATH.`, 'workset_tool_unavailable', {
95
+ target: 'workset.tool',
96
+ fix: alternative !== null
97
+ ? `Install '${opener.command}' or run: openspec workset open ${worksetName} --tool ${alternative}`
98
+ : `Install '${opener.command}', then rerun: openspec workset open ${worksetName}`,
99
+ });
100
+ }
101
+ /** Interactive open with no saved tool and nothing installed at all. */
102
+ export function noToolInstalledError(table, worksetName) {
103
+ const commands = table
104
+ .filter((opener) => isOpenerEnabled(opener))
105
+ .map((opener) => opener.command)
106
+ .join(', ');
107
+ return new StoreError('None of the known tools is on PATH.', 'workset_tool_unavailable', {
108
+ target: 'workset.tool',
109
+ fix: `Install one of: ${commands}. Then rerun: openspec workset open ${worksetName}`,
110
+ });
111
+ }
112
+ //# sourceMappingURL=workset-input.js.map
@@ -0,0 +1,12 @@
1
+ import { type OpenerChoice, type OpenerDefinition } from '../core/openers.js';
2
+ import { type Workset } from '../core/worksets.js';
3
+ export interface ComposeInput {
4
+ memberFlags: string[];
5
+ tool?: string;
6
+ }
7
+ export declare function composeInteractively(givenName: string | undefined, input: ComposeInput, table: OpenerDefinition[]): Promise<Workset>;
8
+ export declare function promptToolFromChoices(available: OpenerChoice[]): Promise<string>;
9
+ export declare function promptOpenNow(label: string): Promise<boolean>;
10
+ /** Prints the workset (decision 13: remove shows what it removes). */
11
+ export declare function confirmRemoveInteractively(workset: Workset): Promise<boolean>;
12
+ //# sourceMappingURL=workset-prompts.d.ts.map
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The workset command's interactive prompt flows (the compose wizard,
3
+ * the open-time tool select, the remove confirm). @inquirer is always
4
+ * imported dynamically at the call site - never at module top.
5
+ */
6
+ import * as path from 'node:path';
7
+ import { pathIsDirectory } from '../core/file-state.js';
8
+ import { listOpenerChoices, } from '../core/openers.js';
9
+ import { expandUserPath } from '../core/store/operations.js';
10
+ import { memberLabelProblem, validateWorksetName, } from '../core/worksets.js';
11
+ import { asErrorMessage } from './shared-output.js';
12
+ import { assertKnownTool, finalizeWorkset, formatMemberRows, resolveMemberFlags, } from './workset-input.js';
13
+ export async function composeInteractively(givenName, input, table) {
14
+ const prompts = await import('@inquirer/prompts');
15
+ console.log('[1/3] Name the workset');
16
+ let name;
17
+ if (givenName !== undefined) {
18
+ name = validateWorksetName(givenName);
19
+ console.log(` Workset name: ${name}`);
20
+ }
21
+ else {
22
+ name = await prompts.input({
23
+ message: 'Workset name:',
24
+ required: true,
25
+ validate(value) {
26
+ try {
27
+ validateWorksetName(value);
28
+ return true;
29
+ }
30
+ catch (error) {
31
+ return asErrorMessage(error);
32
+ }
33
+ },
34
+ });
35
+ }
36
+ // Flag-provided pieces are validated before any prompting, so a
37
+ // bad flag or tool cannot discard a finished wizard walk.
38
+ if (input.tool !== undefined) {
39
+ assertKnownTool(input.tool, table);
40
+ }
41
+ console.log('');
42
+ console.log('[2/3] Add member folders (the first one is the primary - sessions start there)');
43
+ const members = await resolveMemberFlags(input.memberFlags);
44
+ if (members.length > 0) {
45
+ finalizeWorkset(name, members, input.tool, table);
46
+ for (const member of members) {
47
+ console.log(` Added '${member.name}' (${member.path})`);
48
+ }
49
+ }
50
+ while (true) {
51
+ if (members.length > 0) {
52
+ const next = await prompts.select({
53
+ message: 'Add another folder or finish:',
54
+ choices: [
55
+ { name: 'Finish', value: 'finish' },
56
+ { name: 'Add another folder', value: 'add' },
57
+ ],
58
+ default: 'finish',
59
+ });
60
+ if (next === 'finish') {
61
+ break;
62
+ }
63
+ }
64
+ const rawPath = await prompts.input({
65
+ message: 'Folder path:',
66
+ ...(members.length === 0 ? { default: '.', prefill: 'editable' } : {}),
67
+ required: true,
68
+ async validate(value) {
69
+ const resolved = path.resolve(expandUserPath(value));
70
+ if (!(await pathIsDirectory(resolved))) {
71
+ return `'${value}' is not an existing folder`;
72
+ }
73
+ return true;
74
+ },
75
+ });
76
+ const resolvedPath = path.resolve(expandUserPath(rawPath));
77
+ let label = path.basename(resolvedPath);
78
+ const collision = members.some((member) => member.name === label);
79
+ if (memberLabelProblem(label) !== null || collision) {
80
+ label = await prompts.input({
81
+ message: 'Name this member (the folder label):',
82
+ required: true,
83
+ validate(value) {
84
+ const problem = memberLabelProblem(value);
85
+ if (problem !== null) {
86
+ return problem;
87
+ }
88
+ if (members.some((member) => member.name === value)) {
89
+ return `duplicate member name '${value}'`;
90
+ }
91
+ return true;
92
+ },
93
+ });
94
+ }
95
+ members.push({ name: label, path: resolvedPath });
96
+ console.log(` Added '${label}' (${resolvedPath})`);
97
+ }
98
+ console.log('');
99
+ console.log('[3/3] Choose your tool');
100
+ let tool = input.tool;
101
+ if (tool === undefined) {
102
+ const choices = listOpenerChoices(table);
103
+ const available = choices.filter((choice) => choice.available);
104
+ if (available.length === 0) {
105
+ console.log(' None of the known tools is on PATH; not saving a preference.');
106
+ console.log(` (Known tools: ${choices.map((choice) => `${choice.opener.id} ${choice.note ?? ''}`.trim()).join(', ')})`);
107
+ }
108
+ else {
109
+ tool = await promptToolFromChoices(available);
110
+ }
111
+ }
112
+ return finalizeWorkset(name, members, tool, table);
113
+ }
114
+ export async function promptToolFromChoices(available) {
115
+ const { select } = await import('@inquirer/prompts');
116
+ return select({
117
+ message: 'Open with:',
118
+ choices: available.map((choice) => ({
119
+ name: choice.opener.label,
120
+ value: choice.opener.id,
121
+ })),
122
+ });
123
+ }
124
+ export async function promptOpenNow(label) {
125
+ const { confirm } = await import('@inquirer/prompts');
126
+ return confirm({
127
+ message: `Open it now in ${label}?`,
128
+ default: true,
129
+ });
130
+ }
131
+ /** Prints the workset (decision 13: remove shows what it removes). */
132
+ export async function confirmRemoveInteractively(workset) {
133
+ const { confirm } = await import('@inquirer/prompts');
134
+ console.log(`Workset '${workset.name}':`);
135
+ for (const row of formatMemberRows(workset.members)) {
136
+ console.log(` ${row}`);
137
+ }
138
+ return confirm({
139
+ message: `Remove workset '${workset.name}'? (member folders are never touched)`,
140
+ default: false,
141
+ });
142
+ }
143
+ //# sourceMappingURL=workset-prompts.js.map