@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
@@ -0,0 +1,251 @@
1
+ import * as nodeFs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import { z } from 'zod';
5
+ import { StoreError } from './store/errors.js';
6
+ import { formatZodIssues } from './zod-issues.js';
7
+ const DEFAULT_ATTACH_FLAG = '--add-dir';
8
+ /**
9
+ * Temporary kill-switch (2026-06): worksets open only in IDE-style
10
+ * ('workspace-file') tools while the CLI-agent ('attach-dirs') open flow
11
+ * is reworked. The agents (Claude Code, codex) launch in a single primary
12
+ * cwd rather than a true combined multi-root view, which makes "where does
13
+ * my change land?" ambiguous. Default off; set
14
+ * OPENSPEC_ENABLE_CLI_AGENT_OPENERS=1 to restore them (internal rollback seam).
15
+ */
16
+ export function isCliAgentOpenersEnabled() {
17
+ return process.env.OPENSPEC_ENABLE_CLI_AGENT_OPENERS === '1';
18
+ }
19
+ /** Whether a tool can be opened right now (CLI-agent styles are gated). */
20
+ export function isOpenerEnabled(opener) {
21
+ return isCliAgentOpenersEnabled() || opener.style !== 'attach-dirs';
22
+ }
23
+ export const BUILTIN_OPENERS = [
24
+ {
25
+ id: 'code',
26
+ label: 'VS Code',
27
+ style: 'workspace-file',
28
+ command: 'code',
29
+ args: [],
30
+ attachFlag: DEFAULT_ATTACH_FLAG,
31
+ },
32
+ {
33
+ id: 'cursor',
34
+ label: 'Cursor',
35
+ style: 'workspace-file',
36
+ command: 'cursor',
37
+ args: [],
38
+ attachFlag: DEFAULT_ATTACH_FLAG,
39
+ },
40
+ {
41
+ id: 'claude',
42
+ label: 'Claude Code',
43
+ style: 'attach-dirs',
44
+ command: 'claude',
45
+ args: [],
46
+ attachFlag: DEFAULT_ATTACH_FLAG,
47
+ },
48
+ {
49
+ id: 'codex',
50
+ label: 'codex',
51
+ style: 'attach-dirs',
52
+ command: 'codex',
53
+ args: ['--sandbox', 'workspace-write'],
54
+ attachFlag: DEFAULT_ATTACH_FLAG,
55
+ },
56
+ ];
57
+ const OPENER_STYLES = ['workspace-file', 'attach-dirs'];
58
+ const OpenerConfigRowSchema = z
59
+ .object({
60
+ style: z.enum(OPENER_STYLES).optional(),
61
+ label: z.string().min(1).optional(),
62
+ command: z.string().min(1).optional(),
63
+ args: z.array(z.string()).optional(),
64
+ attach_flag: z.string().min(1).optional(),
65
+ })
66
+ .strict();
67
+ const OpenersConfigSchema = z.record(z.string(), OpenerConfigRowSchema);
68
+ function invalidOpenerConfigError(message, configPath) {
69
+ return new StoreError(`Invalid openers config: ${message}`, 'invalid_opener_config', {
70
+ target: 'openers.config',
71
+ fix: `Each entry under "openers" in ${configPath} may set style ('workspace-file' or 'attach-dirs'), label, command, args, and attach_flag; new tools must set style.`,
72
+ });
73
+ }
74
+ /**
75
+ * Merges the global config file's raw `openers` value over the
76
+ * built-in table. A row keyed by a built-in id overrides only the
77
+ * fields it sets; a new id adds a tool (style required, command and
78
+ * label default to the id). Malformed rows fail typed - never
79
+ * silently ignored.
80
+ */
81
+ function cloneOpener(opener) {
82
+ return { ...opener, args: [...opener.args] };
83
+ }
84
+ export function mergeOpenerTable(rawOpeners, configPath) {
85
+ if (rawOpeners === undefined || rawOpeners === null) {
86
+ return BUILTIN_OPENERS.map(cloneOpener);
87
+ }
88
+ const result = OpenersConfigSchema.safeParse(rawOpeners);
89
+ if (!result.success) {
90
+ throw invalidOpenerConfigError(formatZodIssues(result.error, 'openers'), configPath);
91
+ }
92
+ const table = BUILTIN_OPENERS.map(cloneOpener);
93
+ for (const [id, row] of Object.entries(result.data)) {
94
+ const builtinIndex = table.findIndex((opener) => opener.id === id);
95
+ if (builtinIndex >= 0) {
96
+ const builtin = table[builtinIndex];
97
+ table[builtinIndex] = {
98
+ ...builtin,
99
+ ...(row.style !== undefined ? { style: row.style } : {}),
100
+ ...(row.label !== undefined ? { label: row.label } : {}),
101
+ ...(row.command !== undefined ? { command: row.command } : {}),
102
+ ...(row.args !== undefined ? { args: row.args } : {}),
103
+ ...(row.attach_flag !== undefined
104
+ ? { attachFlag: row.attach_flag }
105
+ : {}),
106
+ };
107
+ continue;
108
+ }
109
+ if (row.style === undefined) {
110
+ throw invalidOpenerConfigError(`'${id}' adds a new tool and must set style ('workspace-file' or 'attach-dirs')`, configPath);
111
+ }
112
+ table.push({
113
+ id,
114
+ label: row.label ?? id,
115
+ style: row.style,
116
+ command: row.command ?? id,
117
+ args: row.args ?? [],
118
+ attachFlag: row.attach_flag ?? DEFAULT_ATTACH_FLAG,
119
+ });
120
+ }
121
+ return table;
122
+ }
123
+ function getPathValue(env) {
124
+ return env.PATH ?? env.Path ?? env.path ?? '';
125
+ }
126
+ function getPathExtensions(platform, env) {
127
+ if (platform !== 'win32') {
128
+ return [''];
129
+ }
130
+ return (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
131
+ .split(';')
132
+ .map((extension) => extension.trim())
133
+ .filter((extension) => extension.length > 0);
134
+ }
135
+ function defaultIsExecutableFile(candidatePath, platform) {
136
+ try {
137
+ if (!nodeFs.statSync(candidatePath).isFile()) {
138
+ return false;
139
+ }
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ if (platform === 'win32') {
145
+ return true;
146
+ }
147
+ try {
148
+ nodeFs.accessSync(candidatePath, nodeFs.constants.X_OK);
149
+ return true;
150
+ }
151
+ catch {
152
+ return false;
153
+ }
154
+ }
155
+ /**
156
+ * PATH availability scan (ported from the deleted workspace openers
157
+ * at f858c19^, sharpened for injectability: the path module is keyed
158
+ * by the injected platform, and a command already carrying a known
159
+ * executable extension matches as-is).
160
+ */
161
+ export function isOpenerCommandAvailable(command, options = {}) {
162
+ const env = options.env ?? process.env;
163
+ const platform = options.platform ?? os.platform();
164
+ const pathModule = platform === 'win32' ? path.win32 : path.posix;
165
+ const isExecutable = options.isExecutableFile ??
166
+ ((candidate) => defaultIsExecutableFile(candidate, platform));
167
+ const extensions = getPathExtensions(platform, env);
168
+ const lowerCommand = command.toLowerCase();
169
+ const carriesKnownExtension = extensions.some((extension) => extension.length > 0 && lowerCommand.endsWith(extension.toLowerCase()));
170
+ // One suffix policy: a command already carrying a known executable
171
+ // extension matches as-is and never gets a second extension appended
172
+ // - agreeing with spawn-time resolution.
173
+ const suffixes = carriesKnownExtension ? [''] : extensions;
174
+ if (/[\\/]/u.test(command)) {
175
+ // Direct paths additionally match bare even on win32 (the spawn
176
+ // call receives the literal path).
177
+ const directSuffixes = Array.from(new Set(['', ...suffixes]));
178
+ return directSuffixes.some((suffix) => isExecutable(command + suffix));
179
+ }
180
+ for (const directory of getPathValue(env).split(pathModule.delimiter)) {
181
+ if (directory.length === 0) {
182
+ continue;
183
+ }
184
+ if (suffixes.some((suffix) => isExecutable(pathModule.join(directory, command + suffix)))) {
185
+ return true;
186
+ }
187
+ }
188
+ return false;
189
+ }
190
+ /** Table order preserved, available tools first (stable sort). */
191
+ export function listOpenerChoices(table, options = {}) {
192
+ return table
193
+ .filter((opener) => isOpenerEnabled(opener))
194
+ .map((opener) => {
195
+ const available = isOpenerCommandAvailable(opener.command, options);
196
+ return {
197
+ opener,
198
+ available,
199
+ note: available ? null : `(${opener.command} not found on PATH)`,
200
+ };
201
+ })
202
+ .sort((a, b) => {
203
+ if (a.available === b.available) {
204
+ return 0;
205
+ }
206
+ return a.available ? -1 : 1;
207
+ });
208
+ }
209
+ export function findOpener(table, id) {
210
+ return table.find((opener) => opener.id === id) ?? null;
211
+ }
212
+ /**
213
+ * Pure argv builder. workspace-file: pre-args + the generated file's
214
+ * absolute path (which also defuses the cursor shim's `agent`
215
+ * first-arg hijack). attach-dirs: pre-args + one attach flag + path
216
+ * pair per surviving member, the primary included (the locked "one
217
+ * attach flag per member"); never a trailing positional - both agent
218
+ * CLIs would read one as a starter prompt, which 7.1 locks out.
219
+ */
220
+ export function buildLaunchCommand(opener, input) {
221
+ if (input.members.length === 0) {
222
+ throw new Error('buildLaunchCommand requires at least one member.');
223
+ }
224
+ // The no-hijack and no-positional guarantees lean on absolute paths
225
+ // (the child resolves relative argv against its own cwd) - keep the
226
+ // invariant local instead of three modules away.
227
+ if (!path.isAbsolute(input.codeWorkspacePath)) {
228
+ throw new Error(`buildLaunchCommand requires an absolute workspace-file path (got '${input.codeWorkspacePath}').`);
229
+ }
230
+ const cwd = input.members[0].path;
231
+ if (opener.style === 'workspace-file') {
232
+ return {
233
+ executable: opener.command,
234
+ args: [...opener.args, input.codeWorkspacePath],
235
+ cwd,
236
+ label: opener.label,
237
+ style: opener.style,
238
+ };
239
+ }
240
+ return {
241
+ executable: opener.command,
242
+ args: [
243
+ ...opener.args,
244
+ ...input.members.flatMap((member) => [opener.attachFlag, member.path]),
245
+ ],
246
+ cwd,
247
+ label: opener.label,
248
+ style: opener.style,
249
+ };
250
+ }
251
+ //# sourceMappingURL=openers.js.map
@@ -0,0 +1,45 @@
1
+ import { type StoreDiagnostic } from './store/errors.js';
2
+ export declare const OPENSPEC_ROOT_DIR = "openspec";
3
+ export declare const OPENSPEC_CONFIG_YAML = "openspec/config.yaml";
4
+ export declare const OPENSPEC_CONFIG_YML = "openspec/config.yml";
5
+ export declare const OPENSPEC_SPECS_DIR = "openspec/specs";
6
+ export declare const OPENSPEC_CHANGES_DIR = "openspec/changes";
7
+ export declare const OPENSPEC_ARCHIVE_DIR = "openspec/changes/archive";
8
+ export declare const DEFAULT_OPENSPEC_SCHEMA = "spec-driven";
9
+ export declare const DIRECTORY_ANCHOR_FILE_NAME = ".gitkeep";
10
+ export declare const ANCHORED_OPENSPEC_DIRS: readonly ["openspec/specs", "openspec/changes/archive"];
11
+ export interface CreatedPathLedgerEntry {
12
+ relativePath: string;
13
+ absolutePath: string;
14
+ kind: 'directory' | 'file';
15
+ }
16
+ export interface OpenSpecRootInspection {
17
+ present: boolean | null;
18
+ config: {
19
+ present: boolean | null;
20
+ path?: string;
21
+ };
22
+ specs: {
23
+ present: boolean | null;
24
+ };
25
+ changes: {
26
+ present: boolean | null;
27
+ };
28
+ archive: {
29
+ present: boolean | null;
30
+ };
31
+ healthy: boolean;
32
+ diagnostics: StoreDiagnostic[];
33
+ }
34
+ export interface EnsureOpenSpecRootResult {
35
+ inspection: OpenSpecRootInspection;
36
+ createdArtifacts: string[];
37
+ createdPaths: CreatedPathLedgerEntry[];
38
+ }
39
+ export declare function inspectOpenSpecRoot(storeRoot: string): Promise<OpenSpecRootInspection>;
40
+ export interface EnsureOpenSpecRootOptions {
41
+ anchorEmptyDirectories?: boolean;
42
+ }
43
+ export declare function ensureOpenSpecRoot(storeRoot: string, options?: EnsureOpenSpecRootOptions): Promise<EnsureOpenSpecRootResult>;
44
+ export declare function rollbackCreatedPaths(entries: CreatedPathLedgerEntry[]): Promise<void>;
45
+ //# sourceMappingURL=openspec-root.d.ts.map
@@ -0,0 +1,195 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { FileSystemUtils } from '../utils/file-system.js';
4
+ import { serializeConfig } from './config-prompts.js';
5
+ import { makeStoreDiagnostic, } from './store/errors.js';
6
+ export const OPENSPEC_ROOT_DIR = 'openspec';
7
+ export const OPENSPEC_CONFIG_YAML = 'openspec/config.yaml';
8
+ export const OPENSPEC_CONFIG_YML = 'openspec/config.yml';
9
+ export const OPENSPEC_SPECS_DIR = 'openspec/specs';
10
+ export const OPENSPEC_CHANGES_DIR = 'openspec/changes';
11
+ export const OPENSPEC_ARCHIVE_DIR = 'openspec/changes/archive';
12
+ export const DEFAULT_OPENSPEC_SCHEMA = 'spec-driven';
13
+ export const DIRECTORY_ANCHOR_FILE_NAME = '.gitkeep';
14
+ // Git cannot track empty directories, so setup anchors otherwise-empty
15
+ // conventional store directories for teammates who clone the repo later.
16
+ export const ANCHORED_OPENSPEC_DIRS = [OPENSPEC_SPECS_DIR, OPENSPEC_ARCHIVE_DIR];
17
+ async function pathKind(targetPath) {
18
+ try {
19
+ const stat = await fs.stat(targetPath);
20
+ if (stat.isDirectory())
21
+ return 'directory';
22
+ if (stat.isFile())
23
+ return 'file';
24
+ return 'other';
25
+ }
26
+ catch (error) {
27
+ if (typeof error === 'object' &&
28
+ error !== null &&
29
+ 'code' in error &&
30
+ error.code === 'ENOENT') {
31
+ return 'missing';
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ function relativeArtifact(relativePath, kind) {
37
+ const normalized = FileSystemUtils.toPosixPath(relativePath);
38
+ return kind === 'directory' ? `${normalized}/` : normalized;
39
+ }
40
+ function unresolvedInspection() {
41
+ return {
42
+ present: null,
43
+ config: { present: null },
44
+ specs: { present: null },
45
+ changes: { present: null },
46
+ archive: { present: null },
47
+ healthy: false,
48
+ diagnostics: [],
49
+ };
50
+ }
51
+ function missingDirectoryDiagnostic(code, message, target) {
52
+ return makeStoreDiagnostic('error', code, message, { target });
53
+ }
54
+ async function inspectOptionalPlanningDirectory(inspection, storeRoot, key, relativePath, notDirectoryCode, target) {
55
+ const kind = await pathKind(path.join(storeRoot, relativePath));
56
+ inspection[key] = { present: kind === 'directory' };
57
+ if (kind === 'directory' || kind === 'missing')
58
+ return kind;
59
+ inspection.diagnostics.push(missingDirectoryDiagnostic(notDirectoryCode, `${relativePath}/ exists but is not a directory.`, target));
60
+ return kind;
61
+ }
62
+ export async function inspectOpenSpecRoot(storeRoot) {
63
+ const rootKind = await pathKind(storeRoot);
64
+ const inspection = unresolvedInspection();
65
+ if (rootKind === 'missing') {
66
+ inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_store_root_missing', 'Store root does not exist.', 'store.root'));
67
+ return inspection;
68
+ }
69
+ if (rootKind !== 'directory') {
70
+ inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_store_root_not_directory', 'Store root is not a directory.', 'store.root'));
71
+ return inspection;
72
+ }
73
+ const openspecPath = path.join(storeRoot, OPENSPEC_ROOT_DIR);
74
+ const openspecKind = await pathKind(openspecPath);
75
+ inspection.present = openspecKind === 'directory';
76
+ if (openspecKind === 'missing') {
77
+ inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_root_missing', 'Missing openspec/ directory.', 'openspec.root'));
78
+ return inspection;
79
+ }
80
+ if (openspecKind !== 'directory') {
81
+ inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_root_not_directory', 'openspec/ exists but is not a directory.', 'openspec.root'));
82
+ return inspection;
83
+ }
84
+ const configYamlKind = await pathKind(path.join(storeRoot, OPENSPEC_CONFIG_YAML));
85
+ const configYmlKind = await pathKind(path.join(storeRoot, OPENSPEC_CONFIG_YML));
86
+ if (configYamlKind === 'file') {
87
+ inspection.config = { present: true, path: OPENSPEC_CONFIG_YAML };
88
+ }
89
+ else if (configYmlKind === 'file') {
90
+ inspection.config = { present: true, path: OPENSPEC_CONFIG_YML };
91
+ }
92
+ else {
93
+ inspection.config = { present: false };
94
+ if (configYamlKind !== 'missing' || configYmlKind !== 'missing') {
95
+ inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_config_not_file', 'OpenSpec config path exists but is not a file.', 'openspec.config'));
96
+ }
97
+ else {
98
+ inspection.diagnostics.push(missingDirectoryDiagnostic('openspec_config_missing', 'Missing openspec/config.yaml or openspec/config.yml.', 'openspec.config'));
99
+ }
100
+ }
101
+ await inspectOptionalPlanningDirectory(inspection, storeRoot, 'specs', OPENSPEC_SPECS_DIR, 'openspec_specs_not_directory', 'openspec.specs');
102
+ const changesKind = await inspectOptionalPlanningDirectory(inspection, storeRoot, 'changes', OPENSPEC_CHANGES_DIR, 'openspec_changes_not_directory', 'openspec.changes');
103
+ if (changesKind === 'directory') {
104
+ await inspectOptionalPlanningDirectory(inspection, storeRoot, 'archive', OPENSPEC_ARCHIVE_DIR, 'openspec_archive_not_directory', 'openspec.archive');
105
+ }
106
+ else {
107
+ inspection.archive = { present: false };
108
+ }
109
+ inspection.healthy =
110
+ inspection.present === true &&
111
+ inspection.config.present === true &&
112
+ inspection.diagnostics.length === 0;
113
+ return inspection;
114
+ }
115
+ async function ensureDirectory(storeRoot, relativePath, ledger) {
116
+ const absolutePath = path.join(storeRoot, relativePath);
117
+ const kind = await pathKind(absolutePath);
118
+ if (kind === 'directory')
119
+ return;
120
+ if (kind !== 'missing') {
121
+ throw new Error(`${relativePath}/ exists but is not a directory.`);
122
+ }
123
+ await fs.mkdir(absolutePath, { recursive: true });
124
+ ledger.push({
125
+ relativePath: relativeArtifact(relativePath, 'directory'),
126
+ absolutePath,
127
+ kind: 'directory',
128
+ });
129
+ }
130
+ async function ensureDefaultConfig(storeRoot, ledger) {
131
+ const configYamlPath = path.join(storeRoot, OPENSPEC_CONFIG_YAML);
132
+ const configYmlPath = path.join(storeRoot, OPENSPEC_CONFIG_YML);
133
+ const yamlKind = await pathKind(configYamlPath);
134
+ const ymlKind = await pathKind(configYmlPath);
135
+ if (yamlKind === 'file' || ymlKind === 'file')
136
+ return;
137
+ if (yamlKind !== 'missing' || ymlKind !== 'missing') {
138
+ throw new Error('OpenSpec config path exists but is not a file.');
139
+ }
140
+ await FileSystemUtils.writeFile(configYamlPath, serializeConfig({ schema: DEFAULT_OPENSPEC_SCHEMA }));
141
+ ledger.push({
142
+ relativePath: relativeArtifact(OPENSPEC_CONFIG_YAML, 'file'),
143
+ absolutePath: configYamlPath,
144
+ kind: 'file',
145
+ });
146
+ }
147
+ async function ensureDirectoryAnchor(storeRoot, relativeDir, ledger) {
148
+ const directory = path.join(storeRoot, relativeDir);
149
+ if ((await fs.readdir(directory)).length > 0)
150
+ return;
151
+ const relativePath = `${relativeDir}/${DIRECTORY_ANCHOR_FILE_NAME}`;
152
+ const absolutePath = path.join(directory, DIRECTORY_ANCHOR_FILE_NAME);
153
+ await fs.writeFile(absolutePath, '', 'utf-8');
154
+ ledger.push({
155
+ relativePath: relativeArtifact(relativePath, 'file'),
156
+ absolutePath,
157
+ kind: 'file',
158
+ });
159
+ }
160
+ export async function ensureOpenSpecRoot(storeRoot, options = {}) {
161
+ const ledger = [];
162
+ const rootKind = await pathKind(storeRoot);
163
+ if (rootKind === 'missing') {
164
+ await fs.mkdir(storeRoot, { recursive: true });
165
+ }
166
+ else if (rootKind !== 'directory') {
167
+ throw new Error('Store root is not a directory.');
168
+ }
169
+ await ensureDirectory(storeRoot, OPENSPEC_ROOT_DIR, ledger);
170
+ await ensureDirectory(storeRoot, OPENSPEC_SPECS_DIR, ledger);
171
+ await ensureDirectory(storeRoot, OPENSPEC_CHANGES_DIR, ledger);
172
+ await ensureDirectory(storeRoot, OPENSPEC_ARCHIVE_DIR, ledger);
173
+ await ensureDefaultConfig(storeRoot, ledger);
174
+ if (options.anchorEmptyDirectories) {
175
+ for (const relativeDir of ANCHORED_OPENSPEC_DIRS) {
176
+ await ensureDirectoryAnchor(storeRoot, relativeDir, ledger);
177
+ }
178
+ }
179
+ return {
180
+ inspection: await inspectOpenSpecRoot(storeRoot),
181
+ createdArtifacts: ledger.map((entry) => entry.relativePath),
182
+ createdPaths: ledger,
183
+ };
184
+ }
185
+ export async function rollbackCreatedPaths(entries) {
186
+ for (const entry of [...entries].reverse()) {
187
+ if (entry.kind === 'file') {
188
+ await fs.rm(entry.absolutePath, { force: true }).catch(() => undefined);
189
+ }
190
+ else {
191
+ await fs.rmdir(entry.absolutePath).catch(() => undefined);
192
+ }
193
+ }
194
+ }
195
+ //# sourceMappingURL=openspec-root.js.map
@@ -1,4 +1,5 @@
1
1
  import { MarkdownParser } from './markdown-parser.js';
2
+ import { buildCodeFenceMask } from './requirement-text.js';
2
3
  import path from 'path';
3
4
  import { promises as fs } from 'fs';
4
5
  export class ChangeParser extends MarkdownParser {
@@ -148,7 +149,7 @@ export class ChangeParser extends MarkdownParser {
148
149
  parseSectionsFromContent(content) {
149
150
  const normalizedContent = ChangeParser.normalizeContent(content);
150
151
  const lines = normalizedContent.split('\n');
151
- const codeFenceLineMask = ChangeParser.buildCodeFenceMask(lines);
152
+ const codeFenceLineMask = buildCodeFenceMask(lines);
152
153
  const sections = [];
153
154
  const stack = [];
154
155
  for (let i = 0; i < lines.length; i++) {
@@ -11,9 +11,6 @@ export declare class MarkdownParser {
11
11
  private currentLine;
12
12
  constructor(content: string);
13
13
  protected static normalizeContent(content: string): string;
14
- protected static buildCodeFenceMask(lines: string[]): boolean[];
15
- private static getFenceMarker;
16
- private static isClosingFence;
17
14
  parseSpec(name: string): Spec;
18
15
  parseChange(name: string): Change;
19
16
  protected parseSections(): Section[];
@@ -1,3 +1,4 @@
1
+ import { buildCodeFenceMask, extractRequirementText } from './requirement-text.js';
1
2
  export class MarkdownParser {
2
3
  lines;
3
4
  codeFenceLineMask;
@@ -5,47 +6,12 @@ export class MarkdownParser {
5
6
  constructor(content) {
6
7
  const normalized = MarkdownParser.normalizeContent(content);
7
8
  this.lines = normalized.split('\n');
8
- this.codeFenceLineMask = MarkdownParser.buildCodeFenceMask(this.lines);
9
+ this.codeFenceLineMask = buildCodeFenceMask(this.lines);
9
10
  this.currentLine = 0;
10
11
  }
11
12
  static normalizeContent(content) {
12
13
  return content.replace(/\r\n?/g, '\n');
13
14
  }
14
- static buildCodeFenceMask(lines) {
15
- const mask = new Array(lines.length).fill(false);
16
- let activeFence = null;
17
- for (let i = 0; i < lines.length; i++) {
18
- const fence = MarkdownParser.getFenceMarker(lines[i]);
19
- if (!activeFence) {
20
- if (fence) {
21
- activeFence = fence;
22
- mask[i] = true;
23
- }
24
- continue;
25
- }
26
- mask[i] = true;
27
- if (MarkdownParser.isClosingFence(lines[i], activeFence)) {
28
- activeFence = null;
29
- }
30
- }
31
- return mask;
32
- }
33
- static getFenceMarker(line) {
34
- const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
35
- if (!fenceMatch) {
36
- return null;
37
- }
38
- return {
39
- marker: fenceMatch[1][0],
40
- length: fenceMatch[1].length,
41
- };
42
- }
43
- static isClosingFence(line, activeFence) {
44
- const fenceMatch = line.match(/^\s*(`{3,}|~{3,})\s*$/);
45
- return Boolean(fenceMatch &&
46
- fenceMatch[1][0] === activeFence.marker &&
47
- fenceMatch[1].length >= activeFence.length);
48
- }
49
15
  parseSpec(name) {
50
16
  const sections = this.parseSections();
51
17
  const purpose = this.findSection(sections, 'Purpose')?.content || '';
@@ -149,29 +115,9 @@ export class MarkdownParser {
149
115
  parseRequirements(section) {
150
116
  const requirements = [];
151
117
  for (const child of section.children) {
152
- // Extract requirement text from first non-empty content line, fall back to heading
153
- let text = child.title;
154
- // Get content before any child sections (scenarios)
155
- if (child.content.trim()) {
156
- // Split content into lines and find content before any child headers
157
- const lines = child.content.split('\n');
158
- const contentBeforeChildren = [];
159
- for (const line of lines) {
160
- // Stop at child headers (scenarios start with ####)
161
- if (line.trim().startsWith('#')) {
162
- break;
163
- }
164
- contentBeforeChildren.push(line);
165
- }
166
- // Find first non-empty line
167
- const directContent = contentBeforeChildren.join('\n').trim();
168
- if (directContent) {
169
- const firstLine = directContent.split('\n').find(l => l.trim());
170
- if (firstLine) {
171
- text = firstLine.trim();
172
- }
173
- }
174
- }
118
+ // Read the requirement text via the shared reader (multi-line, fence- and
119
+ // metadata-aware, with the shared header-title fallback for empty bodies).
120
+ const text = extractRequirementText(child.title, child.content.split('\n'));
175
121
  const scenarios = this.parseScenarios(child);
176
122
  requirements.push({
177
123
  text,
@@ -15,6 +15,16 @@ export declare function normalizeRequirementName(name: string): string;
15
15
  * Extracts the Requirements section from a spec file and parses requirement blocks.
16
16
  */
17
17
  export declare function extractRequirementsSection(content: string): RequirementsSectionParts;
18
+ /**
19
+ * A level-3 header inside `## ADDED`/`## MODIFIED Requirements` that is not a
20
+ * canonical `### Requirement:` header, recorded at the moment the delta reader
21
+ * skips over it. Surfaced as an INFO note by `validate <change>` (#498).
22
+ */
23
+ export interface SkippedHeader {
24
+ header: string;
25
+ section: string;
26
+ line: number;
27
+ }
18
28
  export interface DeltaPlan {
19
29
  added: RequirementBlock[];
20
30
  modified: RequirementBlock[];
@@ -23,6 +33,7 @@ export interface DeltaPlan {
23
33
  from: string;
24
34
  to: string;
25
35
  }>;
36
+ skippedHeaders: SkippedHeader[];
26
37
  sectionPresence: {
27
38
  added: boolean;
28
39
  modified: boolean;