@fission-ai/openspec 1.3.0 → 1.4.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 (179) hide show
  1. package/README.md +9 -6
  2. package/bin/openspec.js +3 -1
  3. package/dist/cli/index.d.ts +4 -1
  4. package/dist/cli/index.js +46 -2
  5. package/dist/commands/completion.js +7 -0
  6. package/dist/commands/config.js +64 -5
  7. package/dist/commands/context-store.d.ts +3 -0
  8. package/dist/commands/context-store.js +475 -0
  9. package/dist/commands/initiative.d.ts +13 -0
  10. package/dist/commands/initiative.js +318 -0
  11. package/dist/commands/workflow/index.d.ts +2 -0
  12. package/dist/commands/workflow/index.js +1 -0
  13. package/dist/commands/workflow/initiative-link.d.ts +24 -0
  14. package/dist/commands/workflow/initiative-link.js +47 -0
  15. package/dist/commands/workflow/instructions.d.ts +1 -1
  16. package/dist/commands/workflow/instructions.js +46 -83
  17. package/dist/commands/workflow/new-change.d.ts +6 -0
  18. package/dist/commands/workflow/new-change.js +114 -17
  19. package/dist/commands/workflow/set-change.d.ts +13 -0
  20. package/dist/commands/workflow/set-change.js +87 -0
  21. package/dist/commands/workflow/shared.d.ts +5 -3
  22. package/dist/commands/workflow/shared.js +6 -6
  23. package/dist/commands/workflow/status.js +24 -9
  24. package/dist/commands/workflow/templates.js +5 -4
  25. package/dist/commands/workspace/context-status.d.ts +4 -0
  26. package/dist/commands/workspace/context-status.js +59 -0
  27. package/dist/commands/workspace/open-target-selection.d.ts +13 -0
  28. package/dist/commands/workspace/open-target-selection.js +146 -0
  29. package/dist/commands/workspace/open-view.d.ts +62 -0
  30. package/dist/commands/workspace/open-view.js +249 -0
  31. package/dist/commands/workspace/open.d.ts +37 -0
  32. package/dist/commands/workspace/open.js +110 -0
  33. package/dist/commands/workspace/opener-selection.d.ts +11 -0
  34. package/dist/commands/workspace/opener-selection.js +98 -0
  35. package/dist/commands/workspace/operations.d.ts +29 -0
  36. package/dist/commands/workspace/operations.js +543 -0
  37. package/dist/commands/workspace/prompt-theme.d.ts +29 -0
  38. package/dist/commands/workspace/prompt-theme.js +24 -0
  39. package/dist/commands/workspace/registration.d.ts +13 -0
  40. package/dist/commands/workspace/registration.js +84 -0
  41. package/dist/commands/workspace/selection.d.ts +9 -0
  42. package/dist/commands/workspace/selection.js +115 -0
  43. package/dist/commands/workspace/setup-prompts.d.ts +13 -0
  44. package/dist/commands/workspace/setup-prompts.js +121 -0
  45. package/dist/commands/workspace/types.d.ts +103 -0
  46. package/dist/commands/workspace/types.js +36 -0
  47. package/dist/commands/workspace.d.ts +6 -0
  48. package/dist/commands/workspace.js +587 -0
  49. package/dist/core/artifact-graph/index.d.ts +3 -1
  50. package/dist/core/artifact-graph/index.js +1 -0
  51. package/dist/core/artifact-graph/instruction-loader.d.ts +41 -1
  52. package/dist/core/artifact-graph/instruction-loader.js +51 -9
  53. package/dist/core/artifact-graph/outputs.d.ts +14 -0
  54. package/dist/core/artifact-graph/outputs.js +39 -0
  55. package/dist/core/artifact-graph/state.js +2 -25
  56. package/dist/core/artifact-graph/types.d.ts +0 -5
  57. package/dist/core/artifact-graph/types.js +0 -14
  58. package/dist/core/change-metadata/index.d.ts +2 -0
  59. package/dist/core/change-metadata/index.js +2 -0
  60. package/dist/core/change-metadata/schema.d.ts +18 -0
  61. package/dist/core/change-metadata/schema.js +28 -0
  62. package/dist/core/change-status-policy.d.ts +50 -0
  63. package/dist/core/change-status-policy.js +70 -0
  64. package/dist/core/collections/index.d.ts +3 -0
  65. package/dist/core/collections/index.js +3 -0
  66. package/dist/core/collections/initiatives/collection.d.ts +4 -0
  67. package/dist/core/collections/initiatives/collection.js +17 -0
  68. package/dist/core/collections/initiatives/index.d.ts +6 -0
  69. package/dist/core/collections/initiatives/index.js +6 -0
  70. package/dist/core/collections/initiatives/operations.d.ts +49 -0
  71. package/dist/core/collections/initiatives/operations.js +175 -0
  72. package/dist/core/collections/initiatives/resolution.d.ts +87 -0
  73. package/dist/core/collections/initiatives/resolution.js +374 -0
  74. package/dist/core/collections/initiatives/schema.d.ts +41 -0
  75. package/dist/core/collections/initiatives/schema.js +134 -0
  76. package/dist/core/collections/initiatives/templates.d.ts +12 -0
  77. package/dist/core/collections/initiatives/templates.js +90 -0
  78. package/dist/core/collections/runtime.d.ts +46 -0
  79. package/dist/core/collections/runtime.js +194 -0
  80. package/dist/core/completions/command-registry.d.ts +1 -5
  81. package/dist/core/completions/command-registry.js +532 -32
  82. package/dist/core/completions/completion-provider.d.ts +11 -0
  83. package/dist/core/completions/completion-provider.js +27 -0
  84. package/dist/core/completions/generators/bash-generator.d.ts +3 -0
  85. package/dist/core/completions/generators/bash-generator.js +60 -4
  86. package/dist/core/completions/generators/fish-generator.js +3 -0
  87. package/dist/core/completions/generators/powershell-generator.d.ts +3 -0
  88. package/dist/core/completions/generators/powershell-generator.js +63 -4
  89. package/dist/core/completions/generators/zsh-generator.d.ts +3 -0
  90. package/dist/core/completions/generators/zsh-generator.js +46 -22
  91. package/dist/core/completions/installers/zsh-installer.d.ts +0 -8
  92. package/dist/core/completions/installers/zsh-installer.js +3 -31
  93. package/dist/core/completions/shared-flags.d.ts +12 -0
  94. package/dist/core/completions/shared-flags.js +28 -0
  95. package/dist/core/completions/templates/bash-templates.d.ts +1 -1
  96. package/dist/core/completions/templates/bash-templates.js +6 -0
  97. package/dist/core/completions/templates/fish-templates.d.ts +1 -1
  98. package/dist/core/completions/templates/fish-templates.js +6 -0
  99. package/dist/core/completions/templates/powershell-templates.d.ts +1 -1
  100. package/dist/core/completions/templates/powershell-templates.js +9 -0
  101. package/dist/core/completions/templates/zsh-templates.d.ts +1 -1
  102. package/dist/core/completions/templates/zsh-templates.js +9 -0
  103. package/dist/core/completions/types.d.ts +23 -1
  104. package/dist/core/config.js +3 -1
  105. package/dist/core/context-store/binding.d.ts +53 -0
  106. package/dist/core/context-store/binding.js +197 -0
  107. package/dist/core/context-store/errors.d.ts +20 -0
  108. package/dist/core/context-store/errors.js +22 -0
  109. package/dist/core/context-store/foundation.d.ts +55 -0
  110. package/dist/core/context-store/foundation.js +321 -0
  111. package/dist/core/context-store/index.d.ts +6 -0
  112. package/dist/core/context-store/index.js +6 -0
  113. package/dist/core/context-store/operations.d.ts +85 -0
  114. package/dist/core/context-store/operations.js +528 -0
  115. package/dist/core/context-store/registry.d.ts +45 -0
  116. package/dist/core/context-store/registry.js +229 -0
  117. package/dist/core/global-config.d.ts +6 -1
  118. package/dist/core/global-config.js +15 -16
  119. package/dist/core/index.d.ts +5 -1
  120. package/dist/core/index.js +4 -0
  121. package/dist/core/parsers/change-parser.js +7 -3
  122. package/dist/core/parsers/markdown-parser.d.ts +4 -0
  123. package/dist/core/parsers/markdown-parser.js +41 -1
  124. package/dist/core/parsers/requirement-blocks.js +5 -5
  125. package/dist/core/parsers/spec-structure.d.ts +9 -0
  126. package/dist/core/parsers/spec-structure.js +88 -0
  127. package/dist/core/planning-home.d.ts +21 -0
  128. package/dist/core/planning-home.js +108 -0
  129. package/dist/core/profiles.d.ts +1 -1
  130. package/dist/core/profiles.js +1 -1
  131. package/dist/core/specs-apply.js +9 -1
  132. package/dist/core/templates/workflows/apply-change.js +10 -4
  133. package/dist/core/templates/workflows/archive-change.js +21 -15
  134. package/dist/core/templates/workflows/bulk-archive-change.js +14 -10
  135. package/dist/core/templates/workflows/continue-change.js +6 -4
  136. package/dist/core/templates/workflows/explore.js +8 -10
  137. package/dist/core/templates/workflows/ff-change.js +8 -6
  138. package/dist/core/templates/workflows/new-change.js +6 -6
  139. package/dist/core/templates/workflows/onboard.js +13 -15
  140. package/dist/core/templates/workflows/propose.js +8 -6
  141. package/dist/core/templates/workflows/sync-specs.js +28 -10
  142. package/dist/core/templates/workflows/verify-change.js +16 -10
  143. package/dist/core/update.d.ts +5 -0
  144. package/dist/core/update.js +20 -0
  145. package/dist/core/validation/validator.d.ts +11 -0
  146. package/dist/core/validation/validator.js +28 -2
  147. package/dist/core/workspace/foundation.d.ts +67 -0
  148. package/dist/core/workspace/foundation.js +295 -0
  149. package/dist/core/workspace/index.d.ts +8 -0
  150. package/dist/core/workspace/index.js +8 -0
  151. package/dist/core/workspace/legacy-state.d.ts +28 -0
  152. package/dist/core/workspace/legacy-state.js +200 -0
  153. package/dist/core/workspace/link-input.d.ts +9 -0
  154. package/dist/core/workspace/link-input.js +32 -0
  155. package/dist/core/workspace/open-surface.d.ts +45 -0
  156. package/dist/core/workspace/open-surface.js +215 -0
  157. package/dist/core/workspace/openers.d.ts +21 -0
  158. package/dist/core/workspace/openers.js +124 -0
  159. package/dist/core/workspace/registry.d.ts +24 -0
  160. package/dist/core/workspace/registry.js +146 -0
  161. package/dist/core/workspace/skills.d.ts +57 -0
  162. package/dist/core/workspace/skills.js +334 -0
  163. package/dist/core/workspace/state-io.d.ts +10 -0
  164. package/dist/core/workspace/state-io.js +119 -0
  165. package/dist/telemetry/config.d.ts +7 -1
  166. package/dist/telemetry/config.js +87 -19
  167. package/dist/telemetry/index.js +19 -0
  168. package/dist/utils/change-metadata.d.ts +5 -2
  169. package/dist/utils/change-metadata.js +7 -13
  170. package/dist/utils/change-utils.d.ts +9 -0
  171. package/dist/utils/change-utils.js +7 -5
  172. package/dist/utils/file-system.d.ts +5 -0
  173. package/dist/utils/file-system.js +21 -1
  174. package/package.json +2 -1
  175. package/schemas/workspace-planning/schema.yaml +72 -0
  176. package/schemas/workspace-planning/templates/design.md +33 -0
  177. package/schemas/workspace-planning/templates/proposal.md +28 -0
  178. package/schemas/workspace-planning/templates/spec.md +9 -0
  179. package/schemas/workspace-planning/templates/tasks.md +15 -0
@@ -6,38 +6,135 @@
6
6
  import ora from 'ora';
7
7
  import path from 'path';
8
8
  import { createChange, validateChangeName } from '../../utils/change-utils.js';
9
+ import { formatChangeLocation, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
9
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';
10
13
  // -----------------------------------------------------------------------------
11
14
  // Command Implementation
12
15
  // -----------------------------------------------------------------------------
13
- export async function newChangeCommand(name, options) {
14
- if (!name) {
15
- throw new Error('Missing required argument <name>');
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');
28
+ }
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}`);
16
36
  }
17
- const validation = validateChangeName(name);
18
- if (!validation.valid) {
19
- throw new Error(validation.error);
37
+ }
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;
20
52
  }
21
- const projectRoot = process.cwd();
22
- // Validate schema if provided
23
- if (options.schema) {
24
- validateSchemaExists(options.schema, projectRoot);
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}/`);
56
+ console.log(`Schema: ${payload.change.schema}`);
57
+ if (payload.initiative) {
58
+ console.log(`Initiative: ${formatInitiativeLink(payload.initiative)}`);
25
59
  }
26
- const schemaDisplay = options.schema ? ` with schema '${options.schema}'` : '';
27
- const spinner = ora(`Creating change '${name}'${schemaDisplay}...`).start();
60
+ }
61
+ export async function newChangeCommand(name, options) {
62
+ const spinner = options.json ? undefined : ora();
28
63
  try {
29
- const result = await createChange(projectRoot, name, { schema: options.schema });
64
+ if (!name) {
65
+ throw new Error('Missing required argument <name>');
66
+ }
67
+ const validation = validateChangeName(name);
68
+ if (!validation.valid) {
69
+ throw new Error(validation.error);
70
+ }
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
+ });
83
+ }
84
+ // Validate schema if provided
85
+ if (options.schema) {
86
+ validateSchemaExists(options.schema, projectRoot);
87
+ }
88
+ const resolvedSchema = options.schema ?? planningHome.defaultSchema;
89
+ if (spinner) {
90
+ spinner.start(`Creating change '${name}' with schema '${resolvedSchema}'...`);
91
+ }
92
+ const workspaceGoal = planningHome.kind === 'workspace'
93
+ ? options.goal ?? options.description
94
+ : options.goal;
95
+ const result = await createChange(projectRoot, name, {
96
+ schema: options.schema,
97
+ defaultSchema: planningHome.defaultSchema,
98
+ changesDir: planningHome.changesDir,
99
+ metadata: {
100
+ ...(workspaceGoal ? { goal: workspaceGoal } : {}),
101
+ ...(affectedAreas.length > 0 ? { affected_areas: affectedAreas } : {}),
102
+ ...(initiative ? { initiative } : {}),
103
+ },
104
+ });
30
105
  // If description provided, create README.md with description
31
106
  if (options.description) {
32
107
  const { promises: fs } = await import('fs');
33
- const changeDir = path.join(projectRoot, 'openspec', 'changes', name);
34
- const readmePath = path.join(changeDir, 'README.md');
108
+ const readmePath = path.join(result.changeDir, 'README.md');
35
109
  await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8');
36
110
  }
37
- spinner.succeed(`Created change '${name}' at openspec/changes/${name}/ (schema: ${result.schema})`);
111
+ const payload = outputForCreatedChange(name, result.changeDir, result.schema, initiative);
112
+ if (options.json) {
113
+ printJson(payload);
114
+ return;
115
+ }
116
+ 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
+ }
38
127
  }
39
128
  catch (error) {
40
- spinner.fail(`Failed to create change '${name}'`);
129
+ spinner?.stop();
130
+ if (options.json) {
131
+ printJson({
132
+ change: null,
133
+ status: [statusFromError(error)],
134
+ });
135
+ process.exitCode = 1;
136
+ return;
137
+ }
41
138
  throw error;
42
139
  }
43
140
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Set Change Command
3
+ *
4
+ * Mutates checked-in repo-local change metadata.
5
+ */
6
+ export interface SetChangeOptions {
7
+ initiative?: string;
8
+ store?: string;
9
+ storePath?: string;
10
+ json?: boolean;
11
+ }
12
+ export declare function setChangeCommand(name: string | undefined, options: SetChangeOptions): Promise<void>;
13
+ //# sourceMappingURL=set-change.d.ts.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Set Change Command
3
+ *
4
+ * Mutates checked-in repo-local change metadata.
5
+ */
6
+ import path from 'node:path';
7
+ import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
8
+ import { readChangeMetadata, resolveSchemaForChange, writeChangeMetadata, } from '../../utils/change-metadata.js';
9
+ import { validateChangeExists } from './shared.js';
10
+ import { resolveInitiativeLinkReference, } from '../../core/collections/initiatives/index.js';
11
+ import { assertInitiativeReference, assertRepoLocalInitiativeLinkPlanningHome, formatInitiativeLink, printJson, sameInitiativeLink, statusFromError, } from './initiative-link.js';
12
+ function outputForSetChange(id, changeDir, schema, initiative, updated) {
13
+ return {
14
+ change: {
15
+ id,
16
+ path: changeDir,
17
+ metadataPath: path.join(changeDir, '.openspec.yaml'),
18
+ schema,
19
+ },
20
+ initiative,
21
+ updated,
22
+ };
23
+ }
24
+ function printSetChangeHuman(payload) {
25
+ if (!payload.change || !payload.initiative) {
26
+ return;
27
+ }
28
+ const verb = payload.updated ? 'Linked' : 'Change already linked';
29
+ console.log(`${verb}: ${payload.change.id}`);
30
+ console.log(`Initiative: ${formatInitiativeLink(payload.initiative)}`);
31
+ console.log(`Metadata: ${payload.change.metadataPath}`);
32
+ }
33
+ export async function setChangeCommand(name, options) {
34
+ try {
35
+ if (!name) {
36
+ throw new Error('Missing required argument <name>');
37
+ }
38
+ assertInitiativeReference(options.initiative);
39
+ const planningHome = resolveCurrentPlanningHomeSync();
40
+ assertRepoLocalInitiativeLinkPlanningHome(planningHome);
41
+ const projectRoot = planningHome.root;
42
+ const changeName = await validateChangeExists(name, projectRoot, planningHome.changesDir);
43
+ const changeDir = getChangeDir(planningHome, changeName);
44
+ const initiative = await resolveInitiativeLinkReference(options.initiative, {
45
+ store: options.store,
46
+ storePath: options.storePath,
47
+ });
48
+ const existingMetadata = readChangeMetadata(changeDir, projectRoot);
49
+ const metadata = existingMetadata ?? {
50
+ schema: resolveSchemaForChange(changeDir, undefined, projectRoot, { metadata: null }),
51
+ };
52
+ if (sameInitiativeLink(metadata.initiative, initiative)) {
53
+ const payload = outputForSetChange(changeName, changeDir, metadata.schema, initiative, false);
54
+ if (options.json) {
55
+ printJson(payload);
56
+ return;
57
+ }
58
+ printSetChangeHuman(payload);
59
+ return;
60
+ }
61
+ if (metadata.initiative) {
62
+ throw new Error(`Change '${changeName}' is already linked to initiative ${formatInitiativeLink(metadata.initiative)}.`);
63
+ }
64
+ writeChangeMetadata(changeDir, {
65
+ ...metadata,
66
+ initiative,
67
+ }, projectRoot);
68
+ const payload = outputForSetChange(changeName, changeDir, metadata.schema, initiative, true);
69
+ if (options.json) {
70
+ printJson(payload);
71
+ return;
72
+ }
73
+ printSetChangeHuman(payload);
74
+ }
75
+ catch (error) {
76
+ if (options.json) {
77
+ printJson({
78
+ change: null,
79
+ status: [statusFromError(error)],
80
+ });
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+ //# sourceMappingURL=set-change.js.map
@@ -4,6 +4,7 @@
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
8
  export interface TaskItem {
8
9
  id: string;
9
10
  description: string;
@@ -13,7 +14,8 @@ export interface ApplyInstructions {
13
14
  changeName: string;
14
15
  changeDir: string;
15
16
  schemaName: string;
16
- contextFiles: Record<string, string>;
17
+ initiative?: InitiativeLink;
18
+ contextFiles: Record<string, string[]>;
17
19
  progress: {
18
20
  total: number;
19
21
  complete: number;
@@ -41,12 +43,12 @@ export declare function getStatusIndicator(status: 'done' | 'ready' | 'blocked')
41
43
  * Returns the list of available change directory names under openspec/changes/.
42
44
  * Excludes the archive directory and hidden directories.
43
45
  */
44
- export declare function getAvailableChanges(projectRoot: string): Promise<string[]>;
46
+ export declare function getAvailableChanges(projectRoot: string, changesDir?: string): Promise<string[]>;
45
47
  /**
46
48
  * Validates that a change exists and returns available changes if not.
47
49
  * Checks directory existence directly to support scaffolded changes (without proposal.md).
48
50
  */
49
- export declare function validateChangeExists(changeName: string | undefined, projectRoot: string): Promise<string>;
51
+ export declare function validateChangeExists(changeName: string | undefined, projectRoot: string, changesDir?: string): Promise<string>;
50
52
  /**
51
53
  * Validates that a schema exists and returns available schemas if not.
52
54
  *
@@ -56,8 +56,8 @@ export function getStatusIndicator(status) {
56
56
  * Returns the list of available change directory names under openspec/changes/.
57
57
  * Excludes the archive directory and hidden directories.
58
58
  */
59
- export async function getAvailableChanges(projectRoot) {
60
- const changesPath = path.join(projectRoot, 'openspec', 'changes');
59
+ export async function getAvailableChanges(projectRoot, changesDir = path.join(projectRoot, 'openspec', 'changes')) {
60
+ const changesPath = changesDir;
61
61
  try {
62
62
  const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
63
63
  return entries
@@ -74,9 +74,9 @@ export async function getAvailableChanges(projectRoot) {
74
74
  * Validates that a change exists and returns available changes if not.
75
75
  * Checks directory existence directly to support scaffolded changes (without proposal.md).
76
76
  */
77
- export async function validateChangeExists(changeName, projectRoot) {
77
+ export async function validateChangeExists(changeName, projectRoot, changesDir = path.join(projectRoot, 'openspec', 'changes')) {
78
78
  if (!changeName) {
79
- const available = await getAvailableChanges(projectRoot);
79
+ const available = await getAvailableChanges(projectRoot, changesDir);
80
80
  if (available.length === 0) {
81
81
  throw new Error('No changes found. Create one with: openspec new change <name>');
82
82
  }
@@ -88,10 +88,10 @@ export async function validateChangeExists(changeName, projectRoot) {
88
88
  throw new Error(`Invalid change name '${changeName}': ${nameValidation.error}`);
89
89
  }
90
90
  // Check directory existence directly
91
- const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
91
+ const changePath = path.join(changesDir, changeName);
92
92
  const exists = fs.existsSync(changePath) && fs.statSync(changePath).isDirectory();
93
93
  if (!exists) {
94
- const available = await getAvailableChanges(projectRoot);
94
+ const available = await getAvailableChanges(projectRoot, changesDir);
95
95
  if (available.length === 0) {
96
96
  throw new Error(`Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>`);
97
97
  }
@@ -5,21 +5,23 @@
5
5
  */
6
6
  import ora from 'ora';
7
7
  import chalk from 'chalk';
8
+ import { resolveCurrentPlanningHomeSync, getChangeDir } from '../../core/planning-home.js';
8
9
  import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
9
10
  import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
10
11
  // -----------------------------------------------------------------------------
11
12
  // Command Implementation
12
13
  // -----------------------------------------------------------------------------
13
14
  export async function statusCommand(options) {
14
- const spinner = ora('Loading change status...').start();
15
+ const spinner = options.json ? undefined : ora('Loading change status...').start();
15
16
  try {
16
- const projectRoot = process.cwd();
17
+ const planningHome = resolveCurrentPlanningHomeSync();
18
+ const projectRoot = planningHome.root;
17
19
  // Handle no-changes case gracefully — status is informational,
18
20
  // so "no changes" is a valid state, not an error.
19
21
  if (!options.change) {
20
- const available = await getAvailableChanges(projectRoot);
22
+ const available = await getAvailableChanges(projectRoot, planningHome.changesDir);
21
23
  if (available.length === 0) {
22
- spinner.stop();
24
+ spinner?.stop();
23
25
  if (options.json) {
24
26
  console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
25
27
  return;
@@ -28,18 +30,21 @@ export async function statusCommand(options) {
28
30
  return;
29
31
  }
30
32
  // Changes exist but --change not provided
31
- spinner.stop();
33
+ spinner?.stop();
32
34
  throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
33
35
  }
34
- const changeName = await validateChangeExists(options.change, projectRoot);
36
+ const changeName = await validateChangeExists(options.change, projectRoot, planningHome.changesDir);
35
37
  // Validate schema if explicitly provided
36
38
  if (options.schema) {
37
39
  validateSchemaExists(options.schema, projectRoot);
38
40
  }
39
41
  // loadChangeContext will auto-detect schema from metadata if not provided
40
- const context = loadChangeContext(projectRoot, changeName, options.schema);
42
+ const context = loadChangeContext(projectRoot, changeName, options.schema, {
43
+ changeDir: getChangeDir(planningHome, changeName),
44
+ planningHome,
45
+ });
41
46
  const status = formatChangeStatus(context);
42
- spinner.stop();
47
+ spinner?.stop();
43
48
  if (options.json) {
44
49
  console.log(JSON.stringify(status, null, 2));
45
50
  return;
@@ -47,7 +52,7 @@ export async function statusCommand(options) {
47
52
  printStatusText(status);
48
53
  }
49
54
  catch (error) {
50
- spinner.stop();
55
+ spinner?.stop();
51
56
  throw error;
52
57
  }
53
58
  }
@@ -56,6 +61,16 @@ export function printStatusText(status) {
56
61
  const total = status.artifacts.length;
57
62
  console.log(`Change: ${status.changeName}`);
58
63
  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}`);
72
+ console.log(`Change root: ${status.changeRoot}`);
73
+ }
59
74
  console.log(`Progress: ${doneCount}/${total} artifacts complete`);
60
75
  console.log();
61
76
  for (const artifact of status.artifacts) {
@@ -6,12 +6,13 @@
6
6
  import ora from 'ora';
7
7
  import path from 'path';
8
8
  import { resolveSchema, getSchemaDir, ArtifactGraph, } from '../../core/artifact-graph/index.js';
9
+ import { FileSystemUtils } from '../../utils/file-system.js';
9
10
  import { validateSchemaExists, DEFAULT_SCHEMA } from './shared.js';
10
11
  // -----------------------------------------------------------------------------
11
12
  // Command Implementation
12
13
  // -----------------------------------------------------------------------------
13
14
  export async function templatesCommand(options) {
14
- const spinner = ora('Loading templates...').start();
15
+ const spinner = options.json ? undefined : ora('Loading templates...').start();
15
16
  try {
16
17
  const projectRoot = process.cwd();
17
18
  const schemaName = validateSchemaExists(options.schema ?? DEFAULT_SCHEMA, projectRoot);
@@ -40,10 +41,10 @@ export async function templatesCommand(options) {
40
41
  }
41
42
  const templates = graph.getAllArtifacts().map((artifact) => ({
42
43
  artifactId: artifact.id,
43
- templatePath: path.join(schemaDir, 'templates', artifact.template),
44
+ templatePath: FileSystemUtils.canonicalizeExistingPath(path.join(schemaDir, 'templates', artifact.template)),
44
45
  source,
45
46
  }));
46
- spinner.stop();
47
+ spinner?.stop();
47
48
  if (options.json) {
48
49
  const output = {};
49
50
  for (const t of templates) {
@@ -61,7 +62,7 @@ export async function templatesCommand(options) {
61
62
  }
62
63
  }
63
64
  catch (error) {
64
- spinner.stop();
65
+ spinner?.stop();
65
66
  throw error;
66
67
  }
67
68
  }
@@ -0,0 +1,4 @@
1
+ import { type WorkspaceContextState } from '../../core/workspace/index.js';
2
+ import { WorkspaceStatus } from './types.js';
3
+ export declare function collectWorkspaceContextStatuses(context: WorkspaceContextState | null): Promise<WorkspaceStatus[]>;
4
+ //# sourceMappingURL=context-status.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { mountInitiativesCollection, readInitiative, } from '../../core/collections/initiatives/index.js';
2
+ import { formatContextStoreBinding, formatContextStoreBindingSelector, resolveContextStoreBinding, } from '../../core/context-store/index.js';
3
+ import { getWorkspaceContextInitiativeId, } from '../../core/workspace/index.js';
4
+ import { asErrorMessage, makeStatus } from './types.js';
5
+ function contextStoreBindingWarningToStatus(warning) {
6
+ return makeStatus('warning', warning.code, warning.message, {
7
+ target: warning.target ? `workspace.context.store.${warning.target}` : 'workspace.context.store',
8
+ ...(warning.fix ? { fix: warning.fix } : {}),
9
+ });
10
+ }
11
+ export async function collectWorkspaceContextStatuses(context) {
12
+ if (!context) {
13
+ return [];
14
+ }
15
+ const initiativeId = getWorkspaceContextInitiativeId(context);
16
+ const contextStoreLabel = formatContextStoreBinding(context.store);
17
+ const selector = formatContextStoreBindingSelector(context.store);
18
+ let resolvedStore;
19
+ try {
20
+ resolvedStore = await resolveContextStoreBinding(context.store);
21
+ }
22
+ catch (error) {
23
+ return [
24
+ makeStatus('error', 'workspace_context_store_unavailable', `Workspace context store '${contextStoreLabel}' could not be read: ${asErrorMessage(error)}`, {
25
+ target: 'workspace.context.store',
26
+ fix: context.store.selector.kind === 'registry'
27
+ ? 'openspec context-store doctor'
28
+ : `Check the path in workspace.yaml or run openspec initiative show ${initiativeId} ${selector}`,
29
+ }),
30
+ ];
31
+ }
32
+ const statuses = resolvedStore.warnings.map(contextStoreBindingWarningToStatus);
33
+ try {
34
+ const initiative = await readInitiative({
35
+ collection: mountInitiativesCollection(resolvedStore.root),
36
+ id: initiativeId,
37
+ });
38
+ if (!initiative) {
39
+ return [
40
+ ...statuses,
41
+ makeStatus('error', 'workspace_initiative_missing', `Workspace initiative '${contextStoreLabel}/${initiativeId}' was not found.`, {
42
+ target: 'workspace.context.initiative',
43
+ fix: `openspec initiative show ${initiativeId} ${selector}`,
44
+ }),
45
+ ];
46
+ }
47
+ return statuses;
48
+ }
49
+ catch (error) {
50
+ return [
51
+ ...statuses,
52
+ makeStatus('error', 'workspace_initiative_unavailable', `Workspace initiative '${contextStoreLabel}/${initiativeId}' could not be read: ${asErrorMessage(error)}`, {
53
+ target: 'workspace.context.initiative',
54
+ fix: `openspec initiative show ${initiativeId} ${selector}`,
55
+ }),
56
+ ];
57
+ }
58
+ }
59
+ //# sourceMappingURL=context-status.js.map
@@ -0,0 +1,13 @@
1
+ import { type InitiativeViewReference } from '../../core/collections/initiatives/index.js';
2
+ import { type SelectedWorkspace, type WorkspaceOpenOptions, type WorkspaceStatus } from './types.js';
3
+ export type WorkspaceOpenTarget = {
4
+ kind: 'workspace';
5
+ selected: SelectedWorkspace;
6
+ status: WorkspaceStatus[];
7
+ } | {
8
+ kind: 'initiative';
9
+ initiative: InitiativeViewReference;
10
+ status: WorkspaceStatus[];
11
+ };
12
+ export declare function selectWorkspaceOpenTarget(workspaceName: string | undefined, options: WorkspaceOpenOptions): Promise<WorkspaceOpenTarget>;
13
+ //# sourceMappingURL=open-target-selection.d.ts.map
@@ -0,0 +1,146 @@
1
+ import { InitiativeResolutionError, initiativeDiagnosticFromError, listInitiativeViewReferences, } from '../../core/collections/initiatives/index.js';
2
+ import { createRegisteredContextStoreBinding, sameContextStoreBinding, } from '../../core/context-store/index.js';
3
+ import { findWorkspaceRoot, getWorkspaceContextInitiativeId, listKnownWorkspaceEntries, readWorkspaceViewState, } from '../../core/workspace/index.js';
4
+ import { isInteractive, resolveNoInteractive } from '../../utils/interactive.js';
5
+ import { selectWorkspaceForCommand, selectedWorkspaceFromEntry, selectedWorkspaceFromRoot, } from './selection.js';
6
+ import { WorkspaceCliError, } from './types.js';
7
+ async function readKnownWorkspaceContexts(entries) {
8
+ return Promise.all(entries.map(async (entry) => {
9
+ try {
10
+ return (await readWorkspaceViewState(entry.workspaceRoot)).context;
11
+ }
12
+ catch {
13
+ // Broken workspaces are surfaced by list/doctor; open target selection
14
+ // should not hide otherwise openable initiatives behind unreadable views.
15
+ return null;
16
+ }
17
+ }));
18
+ }
19
+ function workspaceContextMatchesInitiative(context, initiative) {
20
+ return (context !== null &&
21
+ sameContextStoreBinding(context.store, createRegisteredContextStoreBinding(initiative.store)) &&
22
+ getWorkspaceContextInitiativeId(context) === initiative.id);
23
+ }
24
+ function initiativeHasKnownWorkspace(contexts, initiative) {
25
+ return contexts.some((context) => workspaceContextMatchesInitiative(context, initiative));
26
+ }
27
+ function initiativeDiagnosticToWorkspaceStatus(diagnostic) {
28
+ return {
29
+ severity: diagnostic.severity,
30
+ code: diagnostic.code,
31
+ message: diagnostic.message,
32
+ target: diagnostic.target,
33
+ fix: diagnostic.fix,
34
+ details: diagnostic.details,
35
+ };
36
+ }
37
+ async function listOpenableInitiatives(entries) {
38
+ try {
39
+ const [result, contexts] = await Promise.all([
40
+ listInitiativeViewReferences(),
41
+ readKnownWorkspaceContexts(entries),
42
+ ]);
43
+ const initiatives = [];
44
+ for (const initiative of result.initiatives) {
45
+ if (!initiativeHasKnownWorkspace(contexts, initiative)) {
46
+ initiatives.push(initiative);
47
+ }
48
+ }
49
+ return {
50
+ kind: 'listed',
51
+ initiatives,
52
+ status: result.status.map(initiativeDiagnosticToWorkspaceStatus),
53
+ };
54
+ }
55
+ catch (error) {
56
+ const diagnostic = error instanceof InitiativeResolutionError
57
+ ? initiativeDiagnosticFromError(error)
58
+ : {
59
+ severity: 'error',
60
+ code: 'initiative_discovery_failed',
61
+ message: error instanceof Error ? error.message : String(error),
62
+ target: 'initiative',
63
+ fix: 'openspec context-store doctor',
64
+ };
65
+ return {
66
+ kind: 'unavailable',
67
+ initiatives: [],
68
+ status: [initiativeDiagnosticToWorkspaceStatus(diagnostic)],
69
+ error: new WorkspaceCliError(diagnostic.message, diagnostic.code, {
70
+ target: diagnostic.target,
71
+ fix: diagnostic.fix,
72
+ details: diagnostic.details,
73
+ }),
74
+ };
75
+ }
76
+ }
77
+ export async function selectWorkspaceOpenTarget(workspaceName, options) {
78
+ if (workspaceName ||
79
+ options.json ||
80
+ resolveNoInteractive(options) ||
81
+ !isInteractive(options)) {
82
+ return {
83
+ kind: 'workspace',
84
+ selected: await selectWorkspaceForCommand({
85
+ ...options,
86
+ workspace: workspaceName,
87
+ }, 'open', { preferPositionalName: true }),
88
+ status: [],
89
+ };
90
+ }
91
+ const entries = await listKnownWorkspaceEntries();
92
+ const currentWorkspaceRoot = await findWorkspaceRoot(process.cwd());
93
+ if (currentWorkspaceRoot) {
94
+ return {
95
+ kind: 'workspace',
96
+ selected: await selectedWorkspaceFromRoot(currentWorkspaceRoot, entries),
97
+ status: [],
98
+ };
99
+ }
100
+ const listed = await listOpenableInitiatives(entries);
101
+ if (listed.initiatives.length === 0) {
102
+ if (listed.kind === 'unavailable' && entries.length === 0) {
103
+ throw listed.error;
104
+ }
105
+ return {
106
+ kind: 'workspace',
107
+ selected: await selectWorkspaceForCommand(options, 'open', {
108
+ preferPositionalName: true,
109
+ }),
110
+ status: listed.status,
111
+ };
112
+ }
113
+ const { select } = await import('@inquirer/prompts');
114
+ const selected = await select({
115
+ message: 'Select workspace or initiative:',
116
+ choices: [
117
+ ...entries.map((entry) => ({
118
+ name: `Workspace: ${entry.name} (${entry.workspaceRoot})`,
119
+ value: {
120
+ kind: 'workspace',
121
+ entry,
122
+ },
123
+ })),
124
+ ...listed.initiatives.map((initiative) => ({
125
+ name: `Initiative: ${initiative.store}/${initiative.id} - ${initiative.title} (create local workspace view)`,
126
+ value: {
127
+ kind: 'initiative',
128
+ initiative,
129
+ },
130
+ })),
131
+ ],
132
+ });
133
+ if (selected.kind === 'workspace') {
134
+ return {
135
+ kind: 'workspace',
136
+ selected: selectedWorkspaceFromEntry(selected.entry),
137
+ status: listed.status,
138
+ };
139
+ }
140
+ return {
141
+ kind: 'initiative',
142
+ initiative: selected.initiative,
143
+ status: listed.status,
144
+ };
145
+ }
146
+ //# sourceMappingURL=open-target-selection.js.map