@fission-ai/openspec 1.3.1 → 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 (165) hide show
  1. package/README.md +9 -2
  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 +28 -11
  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 +4 -2
  22. package/dist/commands/workflow/shared.js +6 -6
  23. package/dist/commands/workflow/status.js +19 -4
  24. package/dist/commands/workspace/context-status.d.ts +4 -0
  25. package/dist/commands/workspace/context-status.js +59 -0
  26. package/dist/commands/workspace/open-target-selection.d.ts +13 -0
  27. package/dist/commands/workspace/open-target-selection.js +146 -0
  28. package/dist/commands/workspace/open-view.d.ts +62 -0
  29. package/dist/commands/workspace/open-view.js +249 -0
  30. package/dist/commands/workspace/open.d.ts +37 -0
  31. package/dist/commands/workspace/open.js +110 -0
  32. package/dist/commands/workspace/opener-selection.d.ts +11 -0
  33. package/dist/commands/workspace/opener-selection.js +98 -0
  34. package/dist/commands/workspace/operations.d.ts +29 -0
  35. package/dist/commands/workspace/operations.js +543 -0
  36. package/dist/commands/workspace/prompt-theme.d.ts +29 -0
  37. package/dist/commands/workspace/prompt-theme.js +24 -0
  38. package/dist/commands/workspace/registration.d.ts +13 -0
  39. package/dist/commands/workspace/registration.js +84 -0
  40. package/dist/commands/workspace/selection.d.ts +9 -0
  41. package/dist/commands/workspace/selection.js +115 -0
  42. package/dist/commands/workspace/setup-prompts.d.ts +13 -0
  43. package/dist/commands/workspace/setup-prompts.js +121 -0
  44. package/dist/commands/workspace/types.d.ts +103 -0
  45. package/dist/commands/workspace/types.js +36 -0
  46. package/dist/commands/workspace.d.ts +6 -0
  47. package/dist/commands/workspace.js +587 -0
  48. package/dist/core/artifact-graph/index.d.ts +2 -1
  49. package/dist/core/artifact-graph/instruction-loader.d.ts +41 -1
  50. package/dist/core/artifact-graph/instruction-loader.js +46 -6
  51. package/dist/core/artifact-graph/types.d.ts +0 -5
  52. package/dist/core/artifact-graph/types.js +0 -14
  53. package/dist/core/change-metadata/index.d.ts +2 -0
  54. package/dist/core/change-metadata/index.js +2 -0
  55. package/dist/core/change-metadata/schema.d.ts +18 -0
  56. package/dist/core/change-metadata/schema.js +28 -0
  57. package/dist/core/change-status-policy.d.ts +50 -0
  58. package/dist/core/change-status-policy.js +70 -0
  59. package/dist/core/collections/index.d.ts +3 -0
  60. package/dist/core/collections/index.js +3 -0
  61. package/dist/core/collections/initiatives/collection.d.ts +4 -0
  62. package/dist/core/collections/initiatives/collection.js +17 -0
  63. package/dist/core/collections/initiatives/index.d.ts +6 -0
  64. package/dist/core/collections/initiatives/index.js +6 -0
  65. package/dist/core/collections/initiatives/operations.d.ts +49 -0
  66. package/dist/core/collections/initiatives/operations.js +175 -0
  67. package/dist/core/collections/initiatives/resolution.d.ts +87 -0
  68. package/dist/core/collections/initiatives/resolution.js +374 -0
  69. package/dist/core/collections/initiatives/schema.d.ts +41 -0
  70. package/dist/core/collections/initiatives/schema.js +134 -0
  71. package/dist/core/collections/initiatives/templates.d.ts +12 -0
  72. package/dist/core/collections/initiatives/templates.js +90 -0
  73. package/dist/core/collections/runtime.d.ts +46 -0
  74. package/dist/core/collections/runtime.js +194 -0
  75. package/dist/core/completions/command-registry.d.ts +1 -5
  76. package/dist/core/completions/command-registry.js +532 -32
  77. package/dist/core/completions/completion-provider.d.ts +11 -0
  78. package/dist/core/completions/completion-provider.js +27 -0
  79. package/dist/core/completions/generators/bash-generator.d.ts +3 -0
  80. package/dist/core/completions/generators/bash-generator.js +60 -4
  81. package/dist/core/completions/generators/fish-generator.js +3 -0
  82. package/dist/core/completions/generators/powershell-generator.d.ts +3 -0
  83. package/dist/core/completions/generators/powershell-generator.js +63 -4
  84. package/dist/core/completions/generators/zsh-generator.d.ts +3 -0
  85. package/dist/core/completions/generators/zsh-generator.js +46 -22
  86. package/dist/core/completions/installers/zsh-installer.d.ts +0 -8
  87. package/dist/core/completions/installers/zsh-installer.js +3 -31
  88. package/dist/core/completions/shared-flags.d.ts +12 -0
  89. package/dist/core/completions/shared-flags.js +28 -0
  90. package/dist/core/completions/templates/bash-templates.d.ts +1 -1
  91. package/dist/core/completions/templates/bash-templates.js +6 -0
  92. package/dist/core/completions/templates/fish-templates.d.ts +1 -1
  93. package/dist/core/completions/templates/fish-templates.js +6 -0
  94. package/dist/core/completions/templates/powershell-templates.d.ts +1 -1
  95. package/dist/core/completions/templates/powershell-templates.js +9 -0
  96. package/dist/core/completions/templates/zsh-templates.d.ts +1 -1
  97. package/dist/core/completions/templates/zsh-templates.js +9 -0
  98. package/dist/core/completions/types.d.ts +23 -1
  99. package/dist/core/config.js +3 -1
  100. package/dist/core/context-store/binding.d.ts +53 -0
  101. package/dist/core/context-store/binding.js +197 -0
  102. package/dist/core/context-store/errors.d.ts +20 -0
  103. package/dist/core/context-store/errors.js +22 -0
  104. package/dist/core/context-store/foundation.d.ts +55 -0
  105. package/dist/core/context-store/foundation.js +321 -0
  106. package/dist/core/context-store/index.d.ts +6 -0
  107. package/dist/core/context-store/index.js +6 -0
  108. package/dist/core/context-store/operations.d.ts +85 -0
  109. package/dist/core/context-store/operations.js +528 -0
  110. package/dist/core/context-store/registry.d.ts +45 -0
  111. package/dist/core/context-store/registry.js +229 -0
  112. package/dist/core/global-config.d.ts +6 -1
  113. package/dist/core/global-config.js +15 -16
  114. package/dist/core/index.d.ts +5 -1
  115. package/dist/core/index.js +4 -0
  116. package/dist/core/parsers/requirement-blocks.js +5 -5
  117. package/dist/core/parsers/spec-structure.js +1 -1
  118. package/dist/core/planning-home.d.ts +21 -0
  119. package/dist/core/planning-home.js +108 -0
  120. package/dist/core/profiles.d.ts +1 -1
  121. package/dist/core/profiles.js +1 -1
  122. package/dist/core/specs-apply.js +1 -1
  123. package/dist/core/templates/workflows/apply-change.js +6 -0
  124. package/dist/core/templates/workflows/archive-change.js +21 -15
  125. package/dist/core/templates/workflows/bulk-archive-change.js +14 -10
  126. package/dist/core/templates/workflows/continue-change.js +6 -4
  127. package/dist/core/templates/workflows/explore.js +8 -10
  128. package/dist/core/templates/workflows/ff-change.js +8 -6
  129. package/dist/core/templates/workflows/new-change.js +6 -6
  130. package/dist/core/templates/workflows/onboard.js +13 -15
  131. package/dist/core/templates/workflows/propose.js +8 -6
  132. package/dist/core/templates/workflows/sync-specs.js +28 -10
  133. package/dist/core/templates/workflows/verify-change.js +10 -4
  134. package/dist/core/update.d.ts +5 -0
  135. package/dist/core/update.js +20 -0
  136. package/dist/core/validation/validator.d.ts +11 -0
  137. package/dist/core/validation/validator.js +19 -2
  138. package/dist/core/workspace/foundation.d.ts +67 -0
  139. package/dist/core/workspace/foundation.js +295 -0
  140. package/dist/core/workspace/index.d.ts +8 -0
  141. package/dist/core/workspace/index.js +8 -0
  142. package/dist/core/workspace/legacy-state.d.ts +28 -0
  143. package/dist/core/workspace/legacy-state.js +200 -0
  144. package/dist/core/workspace/link-input.d.ts +9 -0
  145. package/dist/core/workspace/link-input.js +32 -0
  146. package/dist/core/workspace/open-surface.d.ts +45 -0
  147. package/dist/core/workspace/open-surface.js +215 -0
  148. package/dist/core/workspace/openers.d.ts +21 -0
  149. package/dist/core/workspace/openers.js +124 -0
  150. package/dist/core/workspace/registry.d.ts +24 -0
  151. package/dist/core/workspace/registry.js +146 -0
  152. package/dist/core/workspace/skills.d.ts +57 -0
  153. package/dist/core/workspace/skills.js +334 -0
  154. package/dist/core/workspace/state-io.d.ts +10 -0
  155. package/dist/core/workspace/state-io.js +119 -0
  156. package/dist/utils/change-metadata.d.ts +5 -2
  157. package/dist/utils/change-metadata.js +7 -13
  158. package/dist/utils/change-utils.d.ts +9 -0
  159. package/dist/utils/change-utils.js +7 -5
  160. package/package.json +2 -1
  161. package/schemas/workspace-planning/schema.yaml +72 -0
  162. package/schemas/workspace-planning/templates/design.md +33 -0
  163. package/schemas/workspace-planning/templates/proposal.md +28 -0
  164. package/schemas/workspace-planning/templates/spec.md +9 -0
  165. package/schemas/workspace-planning/templates/tasks.md +15 -0
@@ -0,0 +1,200 @@
1
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
2
+ import { z } from 'zod';
3
+ import { WORKSPACE_METADATA_DIR_NAME, WORKSPACE_VIEW_STATE_FILE_NAME, getWorkspaceMetadataDir, parseWorkspaceViewState, validateWorkspaceLinkName, validateWorkspaceName, validateWorkspacePreferredOpener, } from './foundation.js';
4
+ import { FileSystemUtils } from '../../utils/file-system.js';
5
+ export const WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME = WORKSPACE_VIEW_STATE_FILE_NAME;
6
+ export const WORKSPACE_LEGACY_LOCAL_STATE_FILE_NAME = 'local.yaml';
7
+ export const WORKSPACE_LEGACY_LOCAL_STATE_IGNORE_PATTERN = `${WORKSPACE_METADATA_DIR_NAME}/${WORKSPACE_LEGACY_LOCAL_STATE_FILE_NAME}`;
8
+ function joinWorkspacePath(basePath, ...segments) {
9
+ return FileSystemUtils.joinPath(basePath, ...segments);
10
+ }
11
+ export function getWorkspaceLegacySharedStatePath(workspaceRoot) {
12
+ return joinWorkspacePath(getWorkspaceMetadataDir(workspaceRoot), WORKSPACE_LEGACY_SHARED_STATE_FILE_NAME);
13
+ }
14
+ export function getWorkspaceLegacyLocalStatePath(workspaceRoot) {
15
+ return joinWorkspacePath(getWorkspaceMetadataDir(workspaceRoot), WORKSPACE_LEGACY_LOCAL_STATE_FILE_NAME);
16
+ }
17
+ function isPlainObject(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ const PlainObjectSchema = z.custom(isPlainObject, {
21
+ message: 'must be an object',
22
+ });
23
+ const PreferredOpenerSchema = z
24
+ .object({
25
+ kind: z.enum(['agent', 'editor']),
26
+ id: z.string(),
27
+ })
28
+ .strict();
29
+ const WorkspaceSkillStateSchema = z
30
+ .object({
31
+ selected_agents: z.array(z.string()),
32
+ last_applied_profile: z.enum(['core', 'custom']).optional(),
33
+ last_applied_delivery: z.enum(['both', 'skills', 'commands']).optional(),
34
+ last_applied_workflow_ids: z.array(z.string()).optional(),
35
+ last_applied_at: z.string().optional(),
36
+ })
37
+ .strict();
38
+ const SharedStateSchema = z.object({
39
+ version: z.literal(1),
40
+ name: z.string(),
41
+ context: z.unknown().optional(),
42
+ links: z.record(z.string(), PlainObjectSchema),
43
+ }).strict();
44
+ const LocalStateSchema = z.object({
45
+ version: z.literal(1),
46
+ paths: z.record(z.string(), z.string()),
47
+ preferred_opener: PreferredOpenerSchema.optional(),
48
+ tools: z.array(z.string()).optional(),
49
+ workspace_skills: WorkspaceSkillStateSchema.optional(),
50
+ }).strict();
51
+ function formatZodIssues(error) {
52
+ return error.issues
53
+ .map((issue) => {
54
+ const location = issue.path.length > 0 ? issue.path.join('.') : 'root';
55
+ return `${location}: ${issue.message}`;
56
+ })
57
+ .join('; ');
58
+ }
59
+ function parseYamlObject(content, label) {
60
+ try {
61
+ return parseYaml(content);
62
+ }
63
+ catch (error) {
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ throw new Error(`Invalid ${label}: ${message}`);
66
+ }
67
+ }
68
+ function assertValidMapKeys(keys, validator, label) {
69
+ for (const key of keys) {
70
+ try {
71
+ validator(key);
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ throw new Error(`Invalid ${label} '${key}': ${message}`);
76
+ }
77
+ }
78
+ }
79
+ function normalizeLegacyWorkspaceContext(name, context) {
80
+ return parseWorkspaceViewState(stringifyYaml({
81
+ version: 1,
82
+ name,
83
+ context: context ?? null,
84
+ links: {},
85
+ })).context;
86
+ }
87
+ export function workspaceViewToSharedState(state) {
88
+ return {
89
+ version: 1,
90
+ name: state.name,
91
+ context: state.context,
92
+ links: Object.fromEntries(Object.keys(state.links).map((linkName) => [linkName, {}])),
93
+ };
94
+ }
95
+ export function workspaceViewToLocalState(state) {
96
+ return {
97
+ version: 1,
98
+ paths: Object.fromEntries(Object.entries(state.links).filter((entry) => typeof entry[1] === 'string')),
99
+ ...(state.preferred_opener ? { preferred_opener: state.preferred_opener } : {}),
100
+ ...(state.tools ? { tools: state.tools } : {}),
101
+ ...(state.workspace_skills ? { workspace_skills: state.workspace_skills } : {}),
102
+ };
103
+ }
104
+ export function workspaceStatePartsToViewState(sharedState, localState) {
105
+ const linkNames = new Set([
106
+ ...Object.keys(sharedState.links),
107
+ ...Object.keys(localState?.paths ?? {}),
108
+ ]);
109
+ const links = Object.fromEntries([...linkNames]
110
+ .sort((a, b) => a.localeCompare(b))
111
+ .map((linkName) => [linkName, localState?.paths[linkName] ?? null]));
112
+ return {
113
+ version: 1,
114
+ name: sharedState.name,
115
+ context: sharedState.context,
116
+ links,
117
+ ...(localState?.preferred_opener ? { preferred_opener: localState.preferred_opener } : {}),
118
+ ...(localState?.tools ? { tools: localState.tools } : {}),
119
+ ...(localState?.workspace_skills ? { workspace_skills: localState.workspace_skills } : {}),
120
+ };
121
+ }
122
+ export function parseWorkspaceSharedState(content) {
123
+ const raw = parseYamlObject(content, 'workspace shared state');
124
+ try {
125
+ return workspaceViewToSharedState(parseWorkspaceViewState(content));
126
+ }
127
+ catch {
128
+ // Fall through to the legacy shared schema.
129
+ }
130
+ const result = SharedStateSchema.safeParse(raw);
131
+ if (!result.success) {
132
+ throw new Error(`Invalid workspace shared state: ${formatZodIssues(result.error)}`);
133
+ }
134
+ validateWorkspaceName(result.data.name);
135
+ assertValidMapKeys(Object.keys(result.data.links), validateWorkspaceLinkName, 'workspace link name');
136
+ return {
137
+ version: 1,
138
+ name: result.data.name,
139
+ context: normalizeLegacyWorkspaceContext(result.data.name, result.data.context),
140
+ links: result.data.links,
141
+ };
142
+ }
143
+ export function parseWorkspaceLocalState(content) {
144
+ const raw = parseYamlObject(content, 'workspace local state');
145
+ try {
146
+ return workspaceViewToLocalState(parseWorkspaceViewState(content));
147
+ }
148
+ catch {
149
+ // Fall through to the legacy local schema.
150
+ }
151
+ const result = LocalStateSchema.safeParse(raw);
152
+ if (!result.success) {
153
+ throw new Error(`Invalid workspace local state: ${formatZodIssues(result.error)}`);
154
+ }
155
+ assertValidMapKeys(Object.keys(result.data.paths), validateWorkspaceLinkName, 'workspace local path name');
156
+ const preferredOpener = result.data.preferred_opener
157
+ ? validateWorkspacePreferredOpener(result.data.preferred_opener)
158
+ : undefined;
159
+ return {
160
+ version: 1,
161
+ paths: result.data.paths,
162
+ ...(preferredOpener ? { preferred_opener: preferredOpener } : {}),
163
+ ...(result.data.tools ? { tools: result.data.tools } : {}),
164
+ ...(result.data.workspace_skills ? { workspace_skills: result.data.workspace_skills } : {}),
165
+ };
166
+ }
167
+ export function serializeWorkspaceSharedState(state) {
168
+ validateWorkspaceName(state.name);
169
+ assertValidMapKeys(Object.keys(state.links), validateWorkspaceLinkName, 'workspace link name');
170
+ for (const [linkName, linkState] of Object.entries(state.links)) {
171
+ if (!isPlainObject(linkState)) {
172
+ throw new Error(`Invalid workspace link '${linkName}': link state must be an object`);
173
+ }
174
+ }
175
+ return stringifyYaml({
176
+ version: 1,
177
+ name: state.name,
178
+ context: state.context,
179
+ links: state.links,
180
+ });
181
+ }
182
+ export function serializeWorkspaceLocalState(state) {
183
+ assertValidMapKeys(Object.keys(state.paths), validateWorkspaceLinkName, 'workspace local path name');
184
+ for (const [linkName, localPath] of Object.entries(state.paths)) {
185
+ if (typeof localPath !== 'string') {
186
+ throw new Error(`Invalid workspace local path '${linkName}': path must be a string`);
187
+ }
188
+ }
189
+ const preferredOpener = state.preferred_opener
190
+ ? validateWorkspacePreferredOpener(state.preferred_opener)
191
+ : undefined;
192
+ return stringifyYaml({
193
+ version: 1,
194
+ paths: state.paths,
195
+ ...(preferredOpener ? { preferred_opener: preferredOpener } : {}),
196
+ ...(state.tools ? { tools: state.tools } : {}),
197
+ ...(state.workspace_skills ? { workspace_skills: state.workspace_skills } : {}),
198
+ });
199
+ }
200
+ //# sourceMappingURL=legacy-state.js.map
@@ -0,0 +1,9 @@
1
+ export interface WorkspaceParsedLinkInput {
2
+ name?: string;
3
+ pathInput: string;
4
+ }
5
+ export interface WorkspaceLinkInputParseOptions {
6
+ cwd?: string;
7
+ }
8
+ export declare function parseWorkspaceSetupLinkInput(value: string, options?: WorkspaceLinkInputParseOptions): Promise<WorkspaceParsedLinkInput>;
9
+ //# sourceMappingURL=link-input.d.ts.map
@@ -0,0 +1,32 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ const fs = nodeFs.promises;
4
+ async function directoryExists(inputPath, cwd) {
5
+ if (inputPath.length === 0) {
6
+ return false;
7
+ }
8
+ const resolvedPath = path.isAbsolute(inputPath)
9
+ ? path.resolve(inputPath)
10
+ : path.resolve(cwd, inputPath);
11
+ try {
12
+ return (await fs.stat(resolvedPath)).isDirectory();
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export async function parseWorkspaceSetupLinkInput(value, options = {}) {
19
+ const cwd = options.cwd ?? process.cwd();
20
+ if (await directoryExists(value, cwd)) {
21
+ return { pathInput: value };
22
+ }
23
+ const separatorIndex = value.indexOf('=');
24
+ if (separatorIndex === -1) {
25
+ return { pathInput: value };
26
+ }
27
+ return {
28
+ name: value.slice(0, separatorIndex),
29
+ pathInput: value.slice(separatorIndex + 1),
30
+ };
31
+ }
32
+ //# sourceMappingURL=link-input.js.map
@@ -0,0 +1,45 @@
1
+ import { WorkspaceViewState } from './foundation.js';
2
+ export declare const WORKSPACE_GUIDANCE_START_MARKER = "<!-- OPENSPEC:WORKSPACE-GUIDANCE:START -->";
3
+ export declare const WORKSPACE_GUIDANCE_END_MARKER = "<!-- OPENSPEC:WORKSPACE-GUIDANCE:END -->";
4
+ export declare const WORKSPACE_OPEN_ROOT_FOLDER_LABEL = "OpenSpec workspace";
5
+ export declare const WORKSPACE_OPEN_INITIATIVE_FOLDER_LABEL = "Initiative context";
6
+ export declare const WORKSPACE_GUIDANCE_BODY = "# OpenSpec Workspace Guidance\n\nThis directory is an OpenSpec workspace: a local working view over context stores, initiatives, repos, and folders.\n\n- Use this workspace to open the local view of coordinated work.\n- Use initiatives for durable cross-team or cross-repo intent, decisions, requirements, and coordination context.\n- Use repo-local OpenSpec changes for implementation plans owned by a repo or team.\n- Use linked repos and folders to inspect context, understand ownership, and make edits in the place that owns the work.\n- Keep workspace-local files focused on local paths, opener state, agent setup, and other machine-specific view state.\n- Use OpenSpec workspace commands instead of hand-editing `workspace.yaml`.\n- If this workspace contains legacy or beta workspace-level planning files, treat them as compatibility context unless the user explicitly asks to use that beta flow.";
7
+ export interface WorkspaceOpenResolvedContext {
8
+ contextStore: {
9
+ id: string;
10
+ root: string;
11
+ };
12
+ initiative: {
13
+ id: string;
14
+ title: string;
15
+ root: string;
16
+ metadataPath: string;
17
+ storePath: string;
18
+ };
19
+ }
20
+ export interface WorkspaceOpenLink {
21
+ name: string;
22
+ path: string;
23
+ }
24
+ export interface WorkspaceSkippedOpenLink {
25
+ name: string;
26
+ path: string | null;
27
+ reason: 'missing-local-path' | 'path-missing';
28
+ }
29
+ export interface WorkspaceOpenSurfaceLinks {
30
+ links: WorkspaceOpenLink[];
31
+ skipped: WorkspaceSkippedOpenLink[];
32
+ }
33
+ export interface WorkspaceOpenSurfaceGeneration {
34
+ agentsPath: string;
35
+ codeWorkspacePath: string;
36
+ }
37
+ export declare function buildWorkspaceGuidanceBlock(viewState?: WorkspaceViewState, resolvedContext?: WorkspaceOpenResolvedContext | null): string;
38
+ export declare function applyWorkspaceGuidanceBlock(existingContent: string, viewState?: WorkspaceViewState, resolvedContext?: WorkspaceOpenResolvedContext | null): string;
39
+ export declare function buildWorkspaceCodeWorkspaceContent(links: WorkspaceOpenLink[], resolvedContext?: WorkspaceOpenResolvedContext | null): string;
40
+ export declare function writeWorkspaceCodeWorkspaceFile(codeWorkspacePath: string, links: WorkspaceOpenLink[], resolvedContext?: WorkspaceOpenResolvedContext | null): Promise<void>;
41
+ export declare function resolveWorkspaceOpenLinks(viewState: WorkspaceViewState): Promise<WorkspaceOpenSurfaceLinks>;
42
+ export declare function syncWorkspaceOpenSurface(workspaceRoot: string, viewState: WorkspaceViewState, resolvedContext?: WorkspaceOpenResolvedContext | null): Promise<WorkspaceOpenSurfaceLinks & {
43
+ generated: WorkspaceOpenSurfaceGeneration;
44
+ }>;
45
+ //# sourceMappingURL=open-surface.d.ts.map
@@ -0,0 +1,215 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { FileSystemUtils } from '../../utils/file-system.js';
4
+ import { getWorkspaceContextInitiativeId, getWorkspaceCodeWorkspacePath, getWorkspaceCodeWorkspaceFileName, } from './foundation.js';
5
+ const fs = nodeFs.promises;
6
+ export const WORKSPACE_GUIDANCE_START_MARKER = '<!-- OPENSPEC:WORKSPACE-GUIDANCE:START -->';
7
+ export const WORKSPACE_GUIDANCE_END_MARKER = '<!-- OPENSPEC:WORKSPACE-GUIDANCE:END -->';
8
+ export const WORKSPACE_OPEN_ROOT_FOLDER_LABEL = 'OpenSpec workspace';
9
+ export const WORKSPACE_OPEN_INITIATIVE_FOLDER_LABEL = 'Initiative context';
10
+ export const WORKSPACE_GUIDANCE_BODY = `# OpenSpec Workspace Guidance
11
+
12
+ This directory is an OpenSpec workspace: a local working view over context stores, initiatives, repos, and folders.
13
+
14
+ - Use this workspace to open the local view of coordinated work.
15
+ - Use initiatives for durable cross-team or cross-repo intent, decisions, requirements, and coordination context.
16
+ - Use repo-local OpenSpec changes for implementation plans owned by a repo or team.
17
+ - Use linked repos and folders to inspect context, understand ownership, and make edits in the place that owns the work.
18
+ - Keep workspace-local files focused on local paths, opener state, agent setup, and other machine-specific view state.
19
+ - Use OpenSpec workspace commands instead of hand-editing \`workspace.yaml\`.
20
+ - If this workspace contains legacy or beta workspace-level planning files, treat them as compatibility context unless the user explicitly asks to use that beta flow.`;
21
+ async function fileExists(filePath) {
22
+ try {
23
+ return (await fs.stat(filePath)).isFile();
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function directoryExists(dirPath) {
30
+ try {
31
+ return (await fs.stat(dirPath)).isDirectory();
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function formatGuidancePathList(items) {
38
+ if (items.length === 0) {
39
+ return '- None selected yet.';
40
+ }
41
+ return items.map((item) => `- ${item.label}: ${item.path}`).join('\n');
42
+ }
43
+ function buildWorkspaceContextGuidance(viewState, resolvedContext) {
44
+ const linkedRoots = Object.entries(viewState.links)
45
+ .filter((entry) => typeof entry[1] === 'string')
46
+ .sort(([left], [right]) => left.localeCompare(right))
47
+ .map(([name, linkPath]) => ({ label: name, path: linkPath }));
48
+ if (!viewState.context) {
49
+ return `## Local View
50
+
51
+ This workspace is not bound to an initiative. It is still a first-class local view over selected repos or folders.
52
+
53
+ ## Linked Implementation Context
54
+
55
+ ${formatGuidancePathList(linkedRoots)}`;
56
+ }
57
+ const storedContextSelector = viewState.context.store.selector;
58
+ const storedContextStore = viewState.context
59
+ ? storedContextSelector?.kind === 'path'
60
+ ? `${viewState.context.store.id} via ${storedContextSelector.path}`
61
+ : viewState.context.store.id
62
+ : null;
63
+ const storedInitiativeId = viewState.context
64
+ ? getWorkspaceContextInitiativeId(viewState.context)
65
+ : null;
66
+ const contextLines = resolvedContext
67
+ ? [
68
+ `- Context store: ${resolvedContext.contextStore.id} (${resolvedContext.contextStore.root})`,
69
+ `- Initiative: ${resolvedContext.initiative.id} (${resolvedContext.initiative.root})`,
70
+ `- Initiative title: ${resolvedContext.initiative.title}`,
71
+ `- Initiative metadata: ${resolvedContext.initiative.metadataPath}`,
72
+ '- Broader context may exist in the context store, but this workspace opens the selected initiative by default.',
73
+ ].join('\n')
74
+ : [
75
+ `- Context store: ${storedContextStore}`,
76
+ `- Initiative: ${storedInitiativeId}`,
77
+ '- Run `openspec workspace open --json` to refresh resolved local paths for this view.',
78
+ ].join('\n');
79
+ return `## Selected Initiative Context
80
+
81
+ ${contextLines}
82
+
83
+ ## Advisory Edit Boundaries
84
+
85
+ - Treat initiative and context-store files as shared coordination context.
86
+ - Treat linked repos and folders as local implementation context when the user has selected them.
87
+ - These boundaries are advisory in this OpenSpec version; use judgment and repo ownership when editing.
88
+
89
+ ## Linked Implementation Context
90
+
91
+ ${formatGuidancePathList(linkedRoots)}`;
92
+ }
93
+ export function buildWorkspaceGuidanceBlock(viewState, resolvedContext) {
94
+ const contextGuidance = viewState
95
+ ? `\n\n${buildWorkspaceContextGuidance(viewState, resolvedContext)}`
96
+ : '';
97
+ return `${WORKSPACE_GUIDANCE_START_MARKER}
98
+ ${WORKSPACE_GUIDANCE_BODY}${contextGuidance}
99
+ ${WORKSPACE_GUIDANCE_END_MARKER}`;
100
+ }
101
+ export function applyWorkspaceGuidanceBlock(existingContent, viewState, resolvedContext) {
102
+ const block = buildWorkspaceGuidanceBlock(viewState, resolvedContext);
103
+ const startIndex = existingContent.indexOf(WORKSPACE_GUIDANCE_START_MARKER);
104
+ const endIndex = existingContent.indexOf(WORKSPACE_GUIDANCE_END_MARKER);
105
+ if (startIndex !== -1 || endIndex !== -1) {
106
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
107
+ throw new Error('Invalid OpenSpec workspace guidance marker state in AGENTS.md.');
108
+ }
109
+ const before = existingContent.slice(0, startIndex).trimEnd();
110
+ const after = existingContent
111
+ .slice(endIndex + WORKSPACE_GUIDANCE_END_MARKER.length)
112
+ .trimStart();
113
+ const prefix = before.length > 0 ? `${before}\n\n` : '';
114
+ const suffix = after.length > 0 ? `\n\n${after.trimEnd()}\n` : '\n';
115
+ return `${prefix}${block}${suffix}`;
116
+ }
117
+ if (existingContent.trim().length === 0) {
118
+ return `${block}\n`;
119
+ }
120
+ return `${existingContent.trimEnd()}\n\n${block}\n`;
121
+ }
122
+ export function buildWorkspaceCodeWorkspaceContent(links, resolvedContext) {
123
+ const folders = [
124
+ ...links.map((link) => ({
125
+ name: link.name,
126
+ path: link.path,
127
+ })),
128
+ ...(resolvedContext
129
+ ? [
130
+ {
131
+ name: WORKSPACE_OPEN_INITIATIVE_FOLDER_LABEL,
132
+ path: resolvedContext.initiative.root,
133
+ },
134
+ ]
135
+ : []),
136
+ {
137
+ name: WORKSPACE_OPEN_ROOT_FOLDER_LABEL,
138
+ path: '.',
139
+ },
140
+ ];
141
+ return `${JSON.stringify({ folders }, null, 2)}\n`;
142
+ }
143
+ export async function writeWorkspaceCodeWorkspaceFile(codeWorkspacePath, links, resolvedContext) {
144
+ await FileSystemUtils.writeFile(codeWorkspacePath, buildWorkspaceCodeWorkspaceContent(links, resolvedContext));
145
+ }
146
+ export async function resolveWorkspaceOpenLinks(viewState) {
147
+ const links = [];
148
+ const skipped = [];
149
+ for (const linkName of Object.keys(viewState.links).sort((a, b) => a.localeCompare(b))) {
150
+ const localPath = viewState.links[linkName] ?? null;
151
+ if (!localPath) {
152
+ skipped.push({
153
+ name: linkName,
154
+ path: null,
155
+ reason: 'missing-local-path',
156
+ });
157
+ continue;
158
+ }
159
+ if (!(await directoryExists(localPath))) {
160
+ skipped.push({
161
+ name: linkName,
162
+ path: localPath,
163
+ reason: 'path-missing',
164
+ });
165
+ continue;
166
+ }
167
+ links.push({
168
+ name: linkName,
169
+ path: localPath,
170
+ });
171
+ }
172
+ return { links, skipped };
173
+ }
174
+ async function syncWorkspaceGuidance(workspaceRoot, viewState, resolvedContext) {
175
+ const agentsPath = path.join(workspaceRoot, 'AGENTS.md');
176
+ const existingContent = (await fileExists(agentsPath))
177
+ ? await fs.readFile(agentsPath, 'utf-8')
178
+ : '';
179
+ await FileSystemUtils.writeFile(agentsPath, applyWorkspaceGuidanceBlock(existingContent, viewState, resolvedContext));
180
+ return agentsPath;
181
+ }
182
+ async function syncWorkspaceCodeWorkspace(workspaceRoot, viewState, links, resolvedContext) {
183
+ const codeWorkspacePath = getWorkspaceCodeWorkspacePath(workspaceRoot, viewState.name);
184
+ await writeWorkspaceCodeWorkspaceFile(codeWorkspacePath, links, resolvedContext);
185
+ return codeWorkspacePath;
186
+ }
187
+ async function cleanupLegacyWorkspaceIgnoreRules(workspaceRoot, workspaceName) {
188
+ const gitignorePath = path.join(workspaceRoot, '.gitignore');
189
+ if (!(await fileExists(gitignorePath))) {
190
+ return;
191
+ }
192
+ const legacyGeneratedPattern = getWorkspaceCodeWorkspaceFileName(workspaceName);
193
+ const existingContent = await fs.readFile(gitignorePath, 'utf-8');
194
+ const existingLines = existingContent.split(/\r?\n/u);
195
+ const nonEmptyLines = existingLines.filter((line) => line.trim().length > 0);
196
+ const isPureLegacyGeneratedFile = nonEmptyLines.length === 1 && nonEmptyLines[0]?.trim() === legacyGeneratedPattern;
197
+ if (!isPureLegacyGeneratedFile) {
198
+ return;
199
+ }
200
+ await fs.rm(gitignorePath, { force: true });
201
+ }
202
+ export async function syncWorkspaceOpenSurface(workspaceRoot, viewState, resolvedContext) {
203
+ const openLinks = await resolveWorkspaceOpenLinks(viewState);
204
+ const agentsPath = await syncWorkspaceGuidance(workspaceRoot, viewState, resolvedContext);
205
+ const codeWorkspacePath = await syncWorkspaceCodeWorkspace(workspaceRoot, viewState, openLinks.links, resolvedContext);
206
+ await cleanupLegacyWorkspaceIgnoreRules(workspaceRoot, viewState.name);
207
+ return {
208
+ ...openLinks,
209
+ generated: {
210
+ agentsPath,
211
+ codeWorkspacePath,
212
+ },
213
+ };
214
+ }
215
+ //# sourceMappingURL=open-surface.js.map
@@ -0,0 +1,21 @@
1
+ import { WorkspacePreferredOpener, WorkspaceSupportedOpenerValue } from './foundation.js';
2
+ export interface WorkspaceOpenerChoice {
3
+ value: WorkspaceSupportedOpenerValue;
4
+ label: string;
5
+ opener: WorkspacePreferredOpener;
6
+ executable: string;
7
+ available: boolean;
8
+ unavailableNote: string | null;
9
+ }
10
+ export declare function isWorkspaceExecutableAvailable(executable: string, options?: {
11
+ env?: NodeJS.ProcessEnv;
12
+ platform?: NodeJS.Platform;
13
+ }): boolean;
14
+ export declare function getWorkspaceOpenerExecutable(opener: WorkspacePreferredOpener): string;
15
+ export declare function getWorkspaceOpenerLabel(opener: WorkspacePreferredOpener): string;
16
+ export declare function listWorkspaceOpenerChoices(options?: {
17
+ env?: NodeJS.ProcessEnv;
18
+ platform?: NodeJS.Platform;
19
+ }): WorkspaceOpenerChoice[];
20
+ export declare function getDefaultWorkspaceOpenerChoiceValue(choices: WorkspaceOpenerChoice[]): WorkspaceSupportedOpenerValue;
21
+ //# sourceMappingURL=openers.d.ts.map
@@ -0,0 +1,124 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { parseWorkspacePreferredOpenerValue, } from './foundation.js';
4
+ const fs = nodeFs;
5
+ const WORKSPACE_OPENER_CHOICE_DEFINITIONS = [
6
+ {
7
+ value: 'editor',
8
+ label: 'VS Code editor',
9
+ executable: 'code',
10
+ },
11
+ {
12
+ value: 'codex-cli',
13
+ label: 'codex-cli',
14
+ executable: 'codex',
15
+ },
16
+ {
17
+ value: 'claude',
18
+ label: 'Claude',
19
+ executable: 'claude',
20
+ },
21
+ {
22
+ value: 'github-copilot',
23
+ label: 'GitHub Copilot in VS Code',
24
+ executable: 'code',
25
+ },
26
+ ];
27
+ function getPathValue(env) {
28
+ return env.PATH ?? env.Path ?? env.path ?? '';
29
+ }
30
+ function getPathExts(env, platform) {
31
+ if (platform !== 'win32') {
32
+ return [''];
33
+ }
34
+ const pathExt = env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD';
35
+ return pathExt
36
+ .split(';')
37
+ .map((extension) => extension.trim())
38
+ .filter((extension) => extension.length > 0);
39
+ }
40
+ function isExecutableFile(candidatePath, platform) {
41
+ try {
42
+ const stats = fs.statSync(candidatePath);
43
+ if (!stats.isFile()) {
44
+ return false;
45
+ }
46
+ if (platform === 'win32') {
47
+ return true;
48
+ }
49
+ fs.accessSync(candidatePath, fs.constants.X_OK);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ export function isWorkspaceExecutableAvailable(executable, options = {}) {
57
+ const env = options.env ?? process.env;
58
+ const platform = options.platform ?? process.platform;
59
+ if (executable.includes('/') || executable.includes('\\')) {
60
+ return isExecutableFile(executable, platform);
61
+ }
62
+ const pathEntries = getPathValue(env)
63
+ .split(path.delimiter)
64
+ .filter((entry) => entry.length > 0);
65
+ const pathExts = getPathExts(env, platform);
66
+ for (const entry of pathEntries) {
67
+ for (const extension of pathExts) {
68
+ const candidate = path.join(entry, executable + extension);
69
+ if (isExecutableFile(candidate, platform)) {
70
+ return true;
71
+ }
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ export function getWorkspaceOpenerExecutable(opener) {
77
+ const openerId = opener.id;
78
+ if (opener.kind === 'editor') {
79
+ return 'code';
80
+ }
81
+ if (openerId === 'github-copilot') {
82
+ return 'code';
83
+ }
84
+ if (openerId === 'codex-cli' || openerId === 'codex') {
85
+ return 'codex';
86
+ }
87
+ return opener.id;
88
+ }
89
+ export function getWorkspaceOpenerLabel(opener) {
90
+ const openerId = opener.id;
91
+ if (opener.kind === 'editor') {
92
+ return 'VS Code editor';
93
+ }
94
+ if (openerId === 'github-copilot') {
95
+ return 'GitHub Copilot in VS Code';
96
+ }
97
+ if (openerId === 'codex-cli' || openerId === 'codex') {
98
+ return 'codex-cli';
99
+ }
100
+ return 'Claude';
101
+ }
102
+ export function listWorkspaceOpenerChoices(options = {}) {
103
+ const choices = WORKSPACE_OPENER_CHOICE_DEFINITIONS.map((definition) => {
104
+ const available = isWorkspaceExecutableAvailable(definition.executable, options);
105
+ return {
106
+ value: definition.value,
107
+ label: definition.label,
108
+ opener: parseWorkspacePreferredOpenerValue(definition.value),
109
+ executable: definition.executable,
110
+ available,
111
+ unavailableNote: available ? null : `${definition.executable} not found on PATH`,
112
+ };
113
+ });
114
+ return choices.sort((a, b) => {
115
+ if (a.available !== b.available) {
116
+ return a.available ? -1 : 1;
117
+ }
118
+ return 0;
119
+ });
120
+ }
121
+ export function getDefaultWorkspaceOpenerChoiceValue(choices) {
122
+ return choices.find((choice) => choice.available)?.value ?? 'editor';
123
+ }
124
+ //# sourceMappingURL=openers.js.map
@@ -0,0 +1,24 @@
1
+ export declare const MANAGED_WORKSPACES_DIR_NAME = "workspaces";
2
+ export declare const WORKSPACE_REGISTRY_FILE_NAME = "registry.yaml";
3
+ export interface WorkspaceRegistryState {
4
+ version: 1;
5
+ workspaces: Record<string, string>;
6
+ }
7
+ export interface WorkspaceRegistryEntry {
8
+ name: string;
9
+ workspaceRoot: string;
10
+ }
11
+ export interface WorkspacePathOptions {
12
+ globalDataDir?: string;
13
+ }
14
+ export declare function getManagedWorkspacesDir(options?: WorkspacePathOptions): string;
15
+ export declare function getManagedWorkspaceRoot(workspaceName: string, options?: WorkspacePathOptions): string;
16
+ export declare function getWorkspaceRegistryPath(options?: WorkspacePathOptions): string;
17
+ export declare function parseWorkspaceRegistryState(content: string): WorkspaceRegistryState;
18
+ export declare function serializeWorkspaceRegistryState(state: WorkspaceRegistryState): string;
19
+ export declare function listWorkspaceRegistryEntries(registry: WorkspaceRegistryState): WorkspaceRegistryEntry[];
20
+ export declare function listKnownWorkspaceEntries(options?: WorkspacePathOptions): Promise<WorkspaceRegistryEntry[]>;
21
+ export declare function listManagedWorkspaceEntries(options?: WorkspacePathOptions): Promise<WorkspaceRegistryEntry[]>;
22
+ export declare function readWorkspaceRegistryState(options?: WorkspacePathOptions): Promise<WorkspaceRegistryState | null>;
23
+ export declare function writeWorkspaceRegistryState(state: WorkspaceRegistryState, options?: WorkspacePathOptions): Promise<void>;
24
+ //# sourceMappingURL=registry.d.ts.map