@fission-ai/openspec 1.6.0 → 1.8.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 (208) hide show
  1. package/README.md +31 -2
  2. package/dist/cli/index.js +79 -8
  3. package/dist/commands/change.d.ts +0 -1
  4. package/dist/commands/change.js +105 -51
  5. package/dist/commands/config.js +14 -8
  6. package/dist/commands/doctor.js +9 -4
  7. package/dist/commands/feedback.js +74 -23
  8. package/dist/commands/schema.js +110 -31
  9. package/dist/commands/show.js +5 -2
  10. package/dist/commands/spec.js +49 -29
  11. package/dist/commands/validate.js +20 -5
  12. package/dist/commands/workflow/index.d.ts +1 -1
  13. package/dist/commands/workflow/index.js +1 -1
  14. package/dist/commands/workflow/instructions.d.ts +7 -1
  15. package/dist/commands/workflow/instructions.js +121 -33
  16. package/dist/commands/workflow/shared.d.ts +13 -2
  17. package/dist/commands/workflow/shared.js +34 -4
  18. package/dist/commands/workflow/status.js +9 -4
  19. package/dist/commands/workflow/templates.js +15 -5
  20. package/dist/core/archive.d.ts +14 -1
  21. package/dist/core/archive.js +1332 -155
  22. package/dist/core/artifact-graph/graph.d.ts +16 -0
  23. package/dist/core/artifact-graph/graph.js +30 -6
  24. package/dist/core/artifact-graph/index.d.ts +1 -1
  25. package/dist/core/artifact-graph/index.js +1 -1
  26. package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
  27. package/dist/core/artifact-graph/instruction-loader.js +70 -12
  28. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  29. package/dist/core/artifact-graph/outputs.js +67 -5
  30. package/dist/core/artifact-graph/resolver.d.ts +14 -0
  31. package/dist/core/artifact-graph/resolver.js +69 -15
  32. package/dist/core/artifact-graph/types.js +21 -3
  33. package/dist/core/available-tools.d.ts +2 -2
  34. package/dist/core/available-tools.js +14 -3
  35. package/dist/core/change-metadata/schema.d.ts +2 -0
  36. package/dist/core/change-metadata/schema.js +13 -0
  37. package/dist/core/change-status-policy.d.ts +1 -1
  38. package/dist/core/change-status-policy.js +2 -2
  39. package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
  40. package/dist/core/command-generation/adapters/amazon-q.js +7 -1
  41. package/dist/core/command-generation/adapters/antigravity.js +2 -1
  42. package/dist/core/command-generation/adapters/auggie.js +2 -1
  43. package/dist/core/command-generation/adapters/bob.d.ts +5 -1
  44. package/dist/core/command-generation/adapters/bob.js +6 -5
  45. package/dist/core/command-generation/adapters/claude.js +1 -8
  46. package/dist/core/command-generation/adapters/codebuddy.js +3 -2
  47. package/dist/core/command-generation/adapters/continue.js +3 -2
  48. package/dist/core/command-generation/adapters/costrict.js +2 -1
  49. package/dist/core/command-generation/adapters/crush.js +5 -5
  50. package/dist/core/command-generation/adapters/cursor.js +2 -2
  51. package/dist/core/command-generation/adapters/devin.d.ts +19 -0
  52. package/dist/core/command-generation/adapters/devin.js +36 -0
  53. package/dist/core/command-generation/adapters/factory.js +2 -1
  54. package/dist/core/command-generation/adapters/gemini.js +37 -2
  55. package/dist/core/command-generation/adapters/github-copilot.js +2 -1
  56. package/dist/core/command-generation/adapters/iflow.js +5 -4
  57. package/dist/core/command-generation/adapters/index.d.ts +2 -2
  58. package/dist/core/command-generation/adapters/index.js +2 -2
  59. package/dist/core/command-generation/adapters/junie.js +2 -1
  60. package/dist/core/command-generation/adapters/kiro.js +2 -1
  61. package/dist/core/command-generation/adapters/lingma.js +5 -5
  62. package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
  63. package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
  64. package/dist/core/command-generation/adapters/opencode.js +3 -5
  65. package/dist/core/command-generation/adapters/pi.d.ts +2 -2
  66. package/dist/core/command-generation/adapters/pi.js +3 -6
  67. package/dist/core/command-generation/adapters/qoder.js +5 -5
  68. package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
  69. package/dist/core/command-generation/adapters/qwen.js +12 -7
  70. package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
  71. package/dist/core/command-generation/adapters/roocode.js +4 -4
  72. package/dist/core/command-generation/adapters/trae.js +1 -21
  73. package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
  74. package/dist/core/command-generation/adapters/zcode.js +33 -0
  75. package/dist/core/command-generation/generator.d.ts +8 -0
  76. package/dist/core/command-generation/generator.js +15 -1
  77. package/dist/core/command-generation/index.d.ts +1 -1
  78. package/dist/core/command-generation/index.js +1 -1
  79. package/dist/core/command-generation/invocation.d.ts +73 -0
  80. package/dist/core/command-generation/invocation.js +77 -0
  81. package/dist/core/command-generation/registry.js +4 -4
  82. package/dist/core/command-generation/types.d.ts +9 -2
  83. package/dist/core/command-generation/yaml.d.ts +20 -6
  84. package/dist/core/command-generation/yaml.js +31 -21
  85. package/dist/core/command-surface.d.ts +15 -0
  86. package/dist/core/command-surface.js +33 -0
  87. package/dist/core/completions/command-registry.js +16 -2
  88. package/dist/core/completions/generators/powershell-generator.js +15 -8
  89. package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
  90. package/dist/core/completions/installers/zsh-installer.js +20 -4
  91. package/dist/core/config-prompts.d.ts +1 -1
  92. package/dist/core/config-prompts.js +14 -1
  93. package/dist/core/config-schema.d.ts +10 -1
  94. package/dist/core/config-schema.js +67 -1
  95. package/dist/core/config.d.ts +15 -0
  96. package/dist/core/config.js +45 -5
  97. package/dist/core/file-state.js +56 -24
  98. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  99. package/dist/core/github-copilot/cloud-agent.js +497 -0
  100. package/dist/core/global-config.d.ts +16 -0
  101. package/dist/core/init.d.ts +58 -0
  102. package/dist/core/init.js +450 -69
  103. package/dist/core/legacy-cleanup.d.ts +59 -0
  104. package/dist/core/legacy-cleanup.js +253 -24
  105. package/dist/core/list.js +5 -7
  106. package/dist/core/migration.d.ts +73 -1
  107. package/dist/core/migration.js +360 -11
  108. package/dist/core/onboarding-commands.d.ts +30 -0
  109. package/dist/core/onboarding-commands.js +38 -0
  110. package/dist/core/parsers/change-parser.d.ts +17 -2
  111. package/dist/core/parsers/change-parser.js +33 -20
  112. package/dist/core/parsers/code-fence.d.ts +15 -0
  113. package/dist/core/parsers/code-fence.js +49 -0
  114. package/dist/core/parsers/markdown-parser.js +2 -1
  115. package/dist/core/parsers/requirement-blocks.d.ts +18 -0
  116. package/dist/core/parsers/requirement-blocks.js +120 -35
  117. package/dist/core/parsers/requirement-text.d.ts +1 -8
  118. package/dist/core/parsers/requirement-text.js +4 -42
  119. package/dist/core/parsers/spec-structure.d.ts +1 -1
  120. package/dist/core/parsers/spec-structure.js +20 -30
  121. package/dist/core/profile-sync-drift.d.ts +0 -8
  122. package/dist/core/profile-sync-drift.js +48 -49
  123. package/dist/core/project-config.d.ts +28 -6
  124. package/dist/core/project-config.js +108 -8
  125. package/dist/core/references.js +42 -4
  126. package/dist/core/relationship-health.d.ts +8 -0
  127. package/dist/core/relationship-health.js +13 -0
  128. package/dist/core/root-selection.d.ts +9 -6
  129. package/dist/core/root-selection.js +42 -5
  130. package/dist/core/shared/index.d.ts +1 -0
  131. package/dist/core/shared/index.js +1 -0
  132. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  133. package/dist/core/shared/skill-content-equivalence.js +41 -0
  134. package/dist/core/shared/skill-paths.d.ts +13 -0
  135. package/dist/core/shared/skill-paths.js +23 -0
  136. package/dist/core/shared/tool-detection.d.ts +25 -5
  137. package/dist/core/shared/tool-detection.js +166 -36
  138. package/dist/core/shared-skill-target.d.ts +16 -0
  139. package/dist/core/shared-skill-target.js +156 -0
  140. package/dist/core/specs-apply.d.ts +80 -33
  141. package/dist/core/specs-apply.js +659 -171
  142. package/dist/core/store/git.d.ts +13 -0
  143. package/dist/core/store/git.js +24 -0
  144. package/dist/core/templates/skill-templates.d.ts +1 -1
  145. package/dist/core/templates/skill-templates.js +1 -1
  146. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  147. package/dist/core/templates/workflows/apply-change.js +48 -158
  148. package/dist/core/templates/workflows/archive-change.js +170 -42
  149. package/dist/core/templates/workflows/bulk-archive-change.js +256 -75
  150. package/dist/core/templates/workflows/continue-change.js +38 -46
  151. package/dist/core/templates/workflows/explore.js +54 -18
  152. package/dist/core/templates/workflows/ff-change.js +46 -30
  153. package/dist/core/templates/workflows/new-change.js +2 -2
  154. package/dist/core/templates/workflows/onboard.js +13 -7
  155. package/dist/core/templates/workflows/propose.js +116 -46
  156. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  157. package/dist/core/templates/workflows/store-selection.js +1 -1
  158. package/dist/core/templates/workflows/sync-specs.js +260 -32
  159. package/dist/core/templates/workflows/update-change.js +26 -16
  160. package/dist/core/templates/workflows/verify-change.js +16 -10
  161. package/dist/core/update.d.ts +40 -5
  162. package/dist/core/update.js +389 -93
  163. package/dist/core/validation/constants.d.ts +4 -1
  164. package/dist/core/validation/constants.js +4 -1
  165. package/dist/core/validation/validator.d.ts +22 -8
  166. package/dist/core/validation/validator.js +222 -50
  167. package/dist/core/version-check.d.ts +152 -0
  168. package/dist/core/version-check.js +731 -0
  169. package/dist/core/view.js +12 -17
  170. package/dist/prompts/searchable-multi-select.js +1 -1
  171. package/dist/telemetry/config.d.ts +3 -4
  172. package/dist/telemetry/index.d.ts +10 -5
  173. package/dist/telemetry/index.js +75 -41
  174. package/dist/ui/welcome-screen.d.ts +9 -1
  175. package/dist/ui/welcome-screen.js +91 -38
  176. package/dist/utils/change-metadata.d.ts +43 -0
  177. package/dist/utils/change-metadata.js +123 -7
  178. package/dist/utils/change-utils.d.ts +10 -4
  179. package/dist/utils/change-utils.js +20 -13
  180. package/dist/utils/ci.d.ts +12 -0
  181. package/dist/utils/ci.js +16 -0
  182. package/dist/utils/command-references.d.ts +92 -6
  183. package/dist/utils/command-references.js +180 -7
  184. package/dist/utils/date.d.ts +8 -0
  185. package/dist/utils/date.js +13 -0
  186. package/dist/utils/file-system.d.ts +10 -0
  187. package/dist/utils/file-system.js +70 -0
  188. package/dist/utils/index.d.ts +1 -1
  189. package/dist/utils/index.js +1 -1
  190. package/dist/utils/interactive.d.ts +23 -0
  191. package/dist/utils/interactive.js +32 -0
  192. package/dist/utils/item-discovery.d.ts +19 -0
  193. package/dist/utils/item-discovery.js +30 -48
  194. package/dist/utils/shell-detection.d.ts +1 -1
  195. package/dist/utils/shell-detection.js +64 -11
  196. package/dist/utils/spec-discovery.d.ts +40 -0
  197. package/dist/utils/spec-discovery.js +115 -0
  198. package/dist/utils/task-progress.d.ts +17 -0
  199. package/dist/utils/task-progress.js +41 -13
  200. package/package.json +14 -12
  201. package/schemas/spec-driven/schema.yaml +67 -10
  202. package/schemas/spec-driven/templates/design.md +2 -2
  203. package/schemas/spec-driven/templates/proposal.md +10 -4
  204. package/schemas/spec-driven/templates/spec.md +3 -0
  205. package/dist/core/command-generation/adapters/codex.d.ts +0 -16
  206. package/dist/core/command-generation/adapters/codex.js +0 -39
  207. package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
  208. package/dist/core/command-generation/adapters/windsurf.js +0 -38
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { getGlobalDataDir } from '../global-config.js';
5
+ import { FileSystemUtils } from '../../utils/file-system.js';
5
6
  import { parseSchema, SchemaValidationError } from './schema.js';
6
7
  /**
7
8
  * Error thrown when loading a schema fails.
@@ -39,6 +40,53 @@ export function getUserSchemasDir() {
39
40
  export function getProjectSchemasDir(projectRoot) {
40
41
  return path.join(projectRoot, 'openspec', 'schemas');
41
42
  }
43
+ /**
44
+ * Determines whether a directory entry represents a schema directory candidate.
45
+ *
46
+ * Returns true for real directories and for symlinks whose target is a
47
+ * directory. `fs.Dirent.isDirectory()` reports the raw entry type, so a symlink
48
+ * (even one pointing at a directory) has `isDirectory() === false`; we
49
+ * dereference such entries via `fs.statSync` to admit symlinked schema dirs
50
+ * while still rejecting symlinks-to-files and broken/dangling symlinks.
51
+ *
52
+ * @param parentDir - The directory containing the entry
53
+ * @param entry - The directory entry from `fs.readdirSync(..., { withFileTypes: true })`
54
+ */
55
+ export function isSchemaDir(parentDir, entry) {
56
+ if (entry.isDirectory()) {
57
+ return true;
58
+ }
59
+ if (entry.isSymbolicLink()) {
60
+ try {
61
+ // statSync follows the link; isDirectory() reflects the target type.
62
+ return fs.statSync(path.join(parentDir, entry.name)).isDirectory();
63
+ }
64
+ catch {
65
+ // Broken symlink (dangling target) — statSync throws; treat as non-dir.
66
+ return false;
67
+ }
68
+ }
69
+ return false;
70
+ }
71
+ /**
72
+ * Returns a schema directory only when its schema file stays within that
73
+ * directory's canonical trust boundary. The directory itself may be a symlink;
74
+ * external user schema links are an intentionally supported workflow.
75
+ */
76
+ function getSchemaCandidateDir(schemasDir, name) {
77
+ const schemaDir = path.join(schemasDir, name);
78
+ const schemaPath = path.join(schemaDir, 'schema.yaml');
79
+ if (!fs.existsSync(schemaPath)) {
80
+ return null;
81
+ }
82
+ try {
83
+ FileSystemUtils.assertPathWithin(schemaDir, schemaPath);
84
+ return schemaDir;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
42
90
  /**
43
91
  * Resolves a schema name to its directory path.
44
92
  *
@@ -55,24 +103,30 @@ export function getProjectSchemasDir(projectRoot) {
55
103
  * @returns The path to the schema directory, or null if not found
56
104
  */
57
105
  export function getSchemaDir(name, projectRoot) {
106
+ if (name.length === 0 ||
107
+ name === '.' ||
108
+ name === '..' ||
109
+ /[\\/]/u.test(name) ||
110
+ /^[A-Za-z]:/u.test(name) ||
111
+ path.posix.isAbsolute(name) ||
112
+ path.win32.isAbsolute(name)) {
113
+ return null;
114
+ }
58
115
  // 1. Check project-local directory (if projectRoot provided)
59
116
  if (projectRoot) {
60
- const projectDir = path.join(getProjectSchemasDir(projectRoot), name);
61
- const projectSchemaPath = path.join(projectDir, 'schema.yaml');
62
- if (fs.existsSync(projectSchemaPath)) {
117
+ const projectDir = getSchemaCandidateDir(getProjectSchemasDir(projectRoot), name);
118
+ if (projectDir) {
63
119
  return projectDir;
64
120
  }
65
121
  }
66
122
  // 2. Check user override directory
67
- const userDir = path.join(getUserSchemasDir(), name);
68
- const userSchemaPath = path.join(userDir, 'schema.yaml');
69
- if (fs.existsSync(userSchemaPath)) {
123
+ const userDir = getSchemaCandidateDir(getUserSchemasDir(), name);
124
+ if (userDir) {
70
125
  return userDir;
71
126
  }
72
127
  // 3. Check package built-in directory
73
- const packageDir = path.join(getPackageSchemasDir(), name);
74
- const packageSchemaPath = path.join(packageDir, 'schema.yaml');
75
- if (fs.existsSync(packageSchemaPath)) {
128
+ const packageDir = getSchemaCandidateDir(getPackageSchemasDir(), name);
129
+ if (packageDir) {
76
130
  return packageDir;
77
131
  }
78
132
  return null;
@@ -134,7 +188,7 @@ export function listSchemas(projectRoot) {
134
188
  const packageDir = getPackageSchemasDir();
135
189
  if (fs.existsSync(packageDir)) {
136
190
  for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
137
- if (entry.isDirectory()) {
191
+ if (isSchemaDir(packageDir, entry)) {
138
192
  const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
139
193
  if (fs.existsSync(schemaPath)) {
140
194
  schemas.add(entry.name);
@@ -146,7 +200,7 @@ export function listSchemas(projectRoot) {
146
200
  const userDir = getUserSchemasDir();
147
201
  if (fs.existsSync(userDir)) {
148
202
  for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
149
- if (entry.isDirectory()) {
203
+ if (isSchemaDir(userDir, entry)) {
150
204
  const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
151
205
  if (fs.existsSync(schemaPath)) {
152
206
  schemas.add(entry.name);
@@ -159,7 +213,7 @@ export function listSchemas(projectRoot) {
159
213
  const projectDir = getProjectSchemasDir(projectRoot);
160
214
  if (fs.existsSync(projectDir)) {
161
215
  for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
162
- if (entry.isDirectory()) {
216
+ if (isSchemaDir(projectDir, entry)) {
163
217
  const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
164
218
  if (fs.existsSync(schemaPath)) {
165
219
  schemas.add(entry.name);
@@ -184,7 +238,7 @@ export function listSchemasWithInfo(projectRoot) {
184
238
  const projectDir = getProjectSchemasDir(projectRoot);
185
239
  if (fs.existsSync(projectDir)) {
186
240
  for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
187
- if (entry.isDirectory()) {
241
+ if (isSchemaDir(projectDir, entry)) {
188
242
  const schemaPath = path.join(projectDir, entry.name, 'schema.yaml');
189
243
  if (fs.existsSync(schemaPath)) {
190
244
  try {
@@ -209,7 +263,7 @@ export function listSchemasWithInfo(projectRoot) {
209
263
  const userDir = getUserSchemasDir();
210
264
  if (fs.existsSync(userDir)) {
211
265
  for (const entry of fs.readdirSync(userDir, { withFileTypes: true })) {
212
- if (entry.isDirectory() && !seenNames.has(entry.name)) {
266
+ if (isSchemaDir(userDir, entry) && !seenNames.has(entry.name)) {
213
267
  const schemaPath = path.join(userDir, entry.name, 'schema.yaml');
214
268
  if (fs.existsSync(schemaPath)) {
215
269
  try {
@@ -233,7 +287,7 @@ export function listSchemasWithInfo(projectRoot) {
233
287
  const packageDir = getPackageSchemasDir();
234
288
  if (fs.existsSync(packageDir)) {
235
289
  for (const entry of fs.readdirSync(packageDir, { withFileTypes: true })) {
236
- if (entry.isDirectory() && !seenNames.has(entry.name)) {
290
+ if (isSchemaDir(packageDir, entry) && !seenNames.has(entry.name)) {
237
291
  const schemaPath = path.join(packageDir, entry.name, 'schema.yaml');
238
292
  if (fs.existsSync(schemaPath)) {
239
293
  try {
@@ -1,10 +1,28 @@
1
+ import * as path from 'node:path';
1
2
  import { z } from 'zod';
3
+ function relativePathSchema(fieldName) {
4
+ return z
5
+ .string()
6
+ .min(1, { error: `${fieldName} is required` })
7
+ .superRefine((value, ctx) => {
8
+ const segments = value.split(/[\\/]+/u);
9
+ const isDrivePath = /^[A-Za-z]:/u.test(value);
10
+ const isAbsolute = path.posix.isAbsolute(value) || path.win32.isAbsolute(value) || isDrivePath;
11
+ const escapes = segments.includes('..');
12
+ if (isAbsolute || escapes || value.includes('\0')) {
13
+ ctx.addIssue({
14
+ code: 'custom',
15
+ message: `${fieldName} must be a relative path inside its allowed directory`,
16
+ });
17
+ }
18
+ });
19
+ }
2
20
  // Artifact definition schema
3
21
  export const ArtifactSchema = z.object({
4
22
  id: z.string().min(1, { error: 'Artifact ID is required' }),
5
- generates: z.string().min(1, { error: 'generates field is required' }),
23
+ generates: relativePathSchema('generates field'),
6
24
  description: z.string(),
7
- template: z.string().min(1, { error: 'template field is required' }),
25
+ template: relativePathSchema('template field'),
8
26
  instruction: z.string().optional(),
9
27
  requires: z.array(z.string()).default([]),
10
28
  });
@@ -13,7 +31,7 @@ export const ApplyPhaseSchema = z.object({
13
31
  // Artifact IDs that must exist before apply is available
14
32
  requires: z.array(z.string()).min(1, { error: 'At least one required artifact' }),
15
33
  // Path to file with checkboxes for progress (relative to change dir), or null if no tracking
16
- tracks: z.string().nullable().optional(),
34
+ tracks: relativePathSchema('apply.tracks').nullable().optional(),
17
35
  // Custom guidance for the apply phase
18
36
  instruction: z.string().optional(),
19
37
  });
@@ -10,8 +10,8 @@ import { type AIToolOption } from './config.js';
10
10
  * the tools that are present.
11
11
  *
12
12
  * For tools with `detectionPaths`, checks those specific paths (files or
13
- * directories). Otherwise checks for the tool's `skillsDir` directory at
14
- * the project root. Only tools with a `skillsDir` property are considered.
13
+ * directories). Otherwise checks the project's `skillsDir`, or managed skill
14
+ * files in the user's home directory for a global skill target.
15
15
  */
16
16
  export declare function getAvailableTools(projectPath: string): AIToolOption[];
17
17
  //# sourceMappingURL=available-tools.d.ts.map
@@ -7,16 +7,25 @@
7
7
  import path from 'path';
8
8
  import * as fs from 'fs';
9
9
  import { AI_TOOLS } from './config.js';
10
+ import { reconcileSharedSkillTargets } from './shared-skill-target.js';
11
+ import { SKILL_NAMES } from './shared/tool-detection.js';
12
+ import { resolveToolSkillsDir, toolSupportsSkills } from './shared/skill-paths.js';
10
13
  /**
11
14
  * Scans the project path for AI tool configuration directories and returns
12
15
  * the tools that are present.
13
16
  *
14
17
  * For tools with `detectionPaths`, checks those specific paths (files or
15
- * directories). Otherwise checks for the tool's `skillsDir` directory at
16
- * the project root. Only tools with a `skillsDir` property are considered.
18
+ * directories). Otherwise checks the project's `skillsDir`, or managed skill
19
+ * files in the user's home directory for a global skill target.
17
20
  */
18
21
  export function getAvailableTools(projectPath) {
19
- return AI_TOOLS.filter((tool) => {
22
+ const available = AI_TOOLS.filter((tool) => {
23
+ if (!toolSupportsSkills(tool))
24
+ return false;
25
+ if (tool.globalSkillsDir) {
26
+ const skillsDir = resolveToolSkillsDir(projectPath, tool);
27
+ return SKILL_NAMES.some((skillName) => fs.existsSync(path.join(skillsDir, skillName, 'SKILL.md')));
28
+ }
20
29
  if (!tool.skillsDir)
21
30
  return false;
22
31
  if (tool.detectionPaths && tool.detectionPaths.length > 0) {
@@ -39,5 +48,7 @@ export function getAvailableTools(projectPath) {
39
48
  return false;
40
49
  }
41
50
  });
51
+ const activeProjectTools = new Set(reconcileSharedSkillTargets(projectPath, available.filter((tool) => tool.skillsDir)).map((tool) => tool.value));
52
+ return available.filter((tool) => tool.globalSkillsDir || activeProjectTools.has(tool.value));
42
53
  }
43
54
  //# sourceMappingURL=available-tools.js.map
@@ -14,6 +14,8 @@ export declare const ChangeMetadataSchema: z.ZodObject<{
14
14
  store: z.ZodString;
15
15
  id: z.ZodString;
16
16
  }, z.core.$strict>>;
17
+ skip_specs: z.ZodOptional<z.ZodBoolean>;
18
+ retire_capabilities: z.ZodOptional<z.ZodBoolean>;
17
19
  }, z.core.$strip>;
18
20
  export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>;
19
21
  //# sourceMappingURL=schema.d.ts.map
@@ -26,5 +26,18 @@ export const ChangeMetadataSchema = z.object({
26
26
  goal: z.string().min(1).optional(),
27
27
  affected_areas: z.array(z.string().min(1)).optional(),
28
28
  initiative: InitiativeLinkSchema.optional(),
29
+ // Declares that this change intentionally has no spec deltas (pure refactor,
30
+ // tooling, or docs work). Validation accepts zero deltas, and the artifact
31
+ // graph counts artifacts whose `generates` path lives under specs/ as
32
+ // complete - that path prefix, not the artifact id, is the contract custom
33
+ // schemas inherit.
34
+ skip_specs: z.boolean().optional(),
35
+ // Declares that this change may retire a capability: when its REMOVED entries
36
+ // take the last requirement a capability has, archive deletes that
37
+ // capability's main spec instead of aborting on a spec it could not write
38
+ // (#1302). Required because the deletion is not recoverable from the working
39
+ // tree - only from git - so it is the author's call, not an inference from the
40
+ // shape of a delta.
41
+ retire_capabilities: z.boolean().optional(),
29
42
  });
30
43
  //# sourceMappingURL=schema.js.map
@@ -18,7 +18,7 @@ export interface ActionContext {
18
18
  }
19
19
  export interface ChangeStatusPolicyArtifact {
20
20
  id: string;
21
- status: 'done' | 'ready' | 'blocked';
21
+ status: 'done' | 'skipped' | 'ready' | 'blocked';
22
22
  }
23
23
  export interface ChangeNextStepsInput {
24
24
  changeName: string;
@@ -23,12 +23,12 @@ export function buildActionContext(input) {
23
23
  export function buildNextSteps(input) {
24
24
  const readyArtifact = input.artifactStatuses.find((artifact) => artifact.status === 'ready');
25
25
  const steps = [];
26
+ const storeFlag = input.storeId ? ` --store ${input.storeId}` : '';
26
27
  if (readyArtifact) {
27
- const storeFlag = input.storeId ? ` --store ${input.storeId}` : '';
28
28
  steps.push(`Run openspec instructions ${readyArtifact.id} --change "${input.changeName}"${storeFlag} --json before writing that artifact.`);
29
29
  }
30
30
  else if (input.allArtifactsComplete) {
31
- steps.push('All planning artifacts are complete; review tasks before implementation.');
31
+ steps.push(`All planning artifacts are complete. Run openspec instructions apply --change "${input.changeName}"${storeFlag} --json to inspect implementation progress.`);
32
32
  }
33
33
  return steps;
34
34
  }
@@ -8,6 +8,10 @@ import type { ToolCommandAdapter } from '../types.js';
8
8
  * Amazon Q adapter for command generation.
9
9
  * File path: .amazonq/prompts/opsx-<id>.md
10
10
  * Frontmatter: description
11
+ *
12
+ * Amazon Q surfaces these files as its prompt library rather than as slash
13
+ * commands: the user types `@opsx-propose`, not `/opsx-propose`.
14
+ * https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-prompts.html
11
15
  */
12
16
  export declare const amazonQAdapter: ToolCommandAdapter;
13
17
  //# sourceMappingURL=amazon-q.d.ts.map
@@ -4,19 +4,25 @@
4
4
  * Formats commands for Amazon Q Developer following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
7
8
  /**
8
9
  * Amazon Q adapter for command generation.
9
10
  * File path: .amazonq/prompts/opsx-<id>.md
10
11
  * Frontmatter: description
12
+ *
13
+ * Amazon Q surfaces these files as its prompt library rather than as slash
14
+ * commands: the user types `@opsx-propose`, not `/opsx-propose`.
15
+ * https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-prompts.html
11
16
  */
12
17
  export const amazonQAdapter = {
13
18
  toolId: 'amazon-q',
14
19
  getFilePath(commandId) {
15
20
  return path.join('.amazonq', 'prompts', `opsx-${commandId}.md`);
16
21
  },
22
+ invocationPrefix: '@',
17
23
  formatFile(content) {
18
24
  return `---
19
- description: ${content.description}
25
+ description: ${escapeYamlValue(content.description)}
20
26
  ---
21
27
 
22
28
  ${content.body}
@@ -4,6 +4,7 @@
4
4
  * Formats commands for Antigravity following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
7
8
  /**
8
9
  * Antigravity adapter for command generation.
9
10
  * File path: .agent/workflows/opsx-<id>.md
@@ -16,7 +17,7 @@ export const antigravityAdapter = {
16
17
  },
17
18
  formatFile(content) {
18
19
  return `---
19
- description: ${content.description}
20
+ description: ${escapeYamlValue(content.description)}
20
21
  ---
21
22
 
22
23
  ${content.body}
@@ -4,6 +4,7 @@
4
4
  * Formats commands for Auggie following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
7
8
  /**
8
9
  * Auggie adapter for command generation.
9
10
  * File path: .augment/commands/opsx-<id>.md
@@ -16,7 +17,7 @@ export const auggieAdapter = {
16
17
  },
17
18
  formatFile(content) {
18
19
  return `---
19
- description: ${content.description}
20
+ description: ${escapeYamlValue(content.description)}
20
21
  argument-hint: command arguments
21
22
  ---
22
23
 
@@ -8,7 +8,11 @@ import type { ToolCommandAdapter } from '../types.js';
8
8
  /**
9
9
  * Bob Shell adapter for command generation.
10
10
  * File path: .bob/commands/opsx-<id>.md
11
- * Frontmatter: description, argument-hint
11
+ * Frontmatter: description
12
+ *
13
+ * Bob uses the filename (minus .md) as the slash command name, so
14
+ * opsx-propose.md → /opsx-propose. generateCommand rewrites the body's
15
+ * command references to that form before this adapter formats it.
12
16
  */
13
17
  export declare const bobAdapter: ToolCommandAdapter;
14
18
  //# sourceMappingURL=bob.d.ts.map
@@ -5,12 +5,15 @@
5
5
  * Commands are stored in .bob/commands/ directory.
6
6
  */
7
7
  import path from 'path';
8
- import { transformToHyphenCommands } from '../../../utils/command-references.js';
9
8
  import { escapeYamlValue } from '../yaml.js';
10
9
  /**
11
10
  * Bob Shell adapter for command generation.
12
11
  * File path: .bob/commands/opsx-<id>.md
13
- * Frontmatter: description, argument-hint
12
+ * Frontmatter: description
13
+ *
14
+ * Bob uses the filename (minus .md) as the slash command name, so
15
+ * opsx-propose.md → /opsx-propose. generateCommand rewrites the body's
16
+ * command references to that form before this adapter formats it.
14
17
  */
15
18
  export const bobAdapter = {
16
19
  toolId: 'bob',
@@ -18,14 +21,12 @@ export const bobAdapter = {
18
21
  return path.join('.bob', 'commands', `opsx-${commandId}.md`);
19
22
  },
20
23
  formatFile(content) {
21
- // Transform command references from colon to hyphen format for Bob
22
- const transformedBody = transformToHyphenCommands(content.body);
23
24
  return `---
24
25
  description: ${escapeYamlValue(content.description)}
25
26
  argument-hint: command arguments
26
27
  ---
27
28
 
28
- ${transformedBody}
29
+ ${content.body}
29
30
  `;
30
31
  },
31
32
  };
@@ -4,15 +4,8 @@
4
4
  * Formats commands for Claude Code following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
- import { escapeYamlValue } from '../yaml.js';
7
+ import { escapeYamlValue, formatTagsArray } from '../yaml.js';
8
8
  import { OPENSPEC_CLI_ALLOWED_TOOLS } from '../../shared/allowed-tools.js';
9
- /**
10
- * Formats a tags array as a YAML array with proper escaping.
11
- */
12
- function formatTagsArray(tags) {
13
- const escapedTags = tags.map((tag) => escapeYamlValue(tag));
14
- return `[${escapedTags.join(', ')}]`;
15
- }
16
9
  /**
17
10
  * Claude Code adapter for command generation.
18
11
  * File path: .claude/commands/opsx/<id>.md
@@ -4,6 +4,7 @@
4
4
  * Formats commands for CodeBuddy following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
7
8
  /**
8
9
  * CodeBuddy adapter for command generation.
9
10
  * File path: .codebuddy/commands/opsx/<id>.md
@@ -16,8 +17,8 @@ export const codebuddyAdapter = {
16
17
  },
17
18
  formatFile(content) {
18
19
  return `---
19
- name: ${content.name}
20
- description: "${content.description}"
20
+ name: ${escapeYamlValue(content.name)}
21
+ description: ${escapeYamlValue(content.description)}
21
22
  argument-hint: "[command arguments]"
22
23
  ---
23
24
 
@@ -4,6 +4,7 @@
4
4
  * Formats commands for Continue following its .prompt specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
7
8
  /**
8
9
  * Continue adapter for command generation.
9
10
  * File path: .continue/prompts/opsx-<id>.prompt
@@ -16,8 +17,8 @@ export const continueAdapter = {
16
17
  },
17
18
  formatFile(content) {
18
19
  return `---
19
- name: opsx-${content.id}
20
- description: ${content.description}
20
+ name: ${escapeYamlValue(`opsx-${content.id}`)}
21
+ description: ${escapeYamlValue(content.description)}
21
22
  invokable: true
22
23
  ---
23
24
 
@@ -4,6 +4,7 @@
4
4
  * Formats commands for CoStrict following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
7
8
  /**
8
9
  * CoStrict adapter for command generation.
9
10
  * File path: .cospec/openspec/commands/opsx-<id>.md
@@ -16,7 +17,7 @@ export const costrictAdapter = {
16
17
  },
17
18
  formatFile(content) {
18
19
  return `---
19
- description: "${content.description}"
20
+ description: ${escapeYamlValue(content.description)}
20
21
  argument-hint: command arguments
21
22
  ---
22
23
 
@@ -4,6 +4,7 @@
4
4
  * Formats commands for Crush following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue, formatTagsArray } from '../yaml.js';
7
8
  /**
8
9
  * Crush adapter for command generation.
9
10
  * File path: .crush/commands/opsx/<id>.md
@@ -15,12 +16,11 @@ export const crushAdapter = {
15
16
  return path.join('.crush', 'commands', 'opsx', `${commandId}.md`);
16
17
  },
17
18
  formatFile(content) {
18
- const tagsStr = content.tags.join(', ');
19
19
  return `---
20
- name: ${content.name}
21
- description: ${content.description}
22
- category: ${content.category}
23
- tags: [${tagsStr}]
20
+ name: ${escapeYamlValue(content.name)}
21
+ description: ${escapeYamlValue(content.description)}
22
+ category: ${escapeYamlValue(content.category)}
23
+ tags: ${formatTagsArray(content.tags)}
24
24
  ---
25
25
 
26
26
  ${content.body}
@@ -18,8 +18,8 @@ export const cursorAdapter = {
18
18
  },
19
19
  formatFile(content) {
20
20
  return `---
21
- name: /opsx-${content.id}
22
- id: opsx-${content.id}
21
+ name: ${escapeYamlValue(`/opsx-${content.id}`)}
22
+ id: ${escapeYamlValue(`opsx-${content.id}`)}
23
23
  category: ${escapeYamlValue(content.category)}
24
24
  description: ${escapeYamlValue(content.description)}
25
25
  ---
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Devin Desktop Command Adapter
3
+ *
4
+ * Formats commands for Devin Desktop following its frontmatter specification.
5
+ * Devin Desktop reads Cascade-style workflows from `.devin/workflows/`, the
6
+ * same shape Windsurf uses.
7
+ */
8
+ import type { ToolCommandAdapter } from '../types.js';
9
+ /**
10
+ * Devin Desktop adapter for command generation.
11
+ * File path: .devin/workflows/opsx-<id>.md
12
+ * Frontmatter: name, description, category, tags
13
+ *
14
+ * The `opsx-` filename prefix makes this a flat invocation, so the generator
15
+ * rewrites the body's `/opsx:*` references to the `/opsx-*` form Devin
16
+ * registers — see invocation.ts.
17
+ */
18
+ export declare const devinAdapter: ToolCommandAdapter;
19
+ //# sourceMappingURL=devin.d.ts.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Devin Desktop Command Adapter
3
+ *
4
+ * Formats commands for Devin Desktop following its frontmatter specification.
5
+ * Devin Desktop reads Cascade-style workflows from `.devin/workflows/`, the
6
+ * same shape Windsurf uses.
7
+ */
8
+ import path from 'path';
9
+ import { escapeYamlValue, formatTagsArray } from '../yaml.js';
10
+ /**
11
+ * Devin Desktop adapter for command generation.
12
+ * File path: .devin/workflows/opsx-<id>.md
13
+ * Frontmatter: name, description, category, tags
14
+ *
15
+ * The `opsx-` filename prefix makes this a flat invocation, so the generator
16
+ * rewrites the body's `/opsx:*` references to the `/opsx-*` form Devin
17
+ * registers — see invocation.ts.
18
+ */
19
+ export const devinAdapter = {
20
+ toolId: 'devin',
21
+ getFilePath(commandId) {
22
+ return path.join('.devin', 'workflows', `opsx-${commandId}.md`);
23
+ },
24
+ formatFile(content) {
25
+ return `---
26
+ name: ${escapeYamlValue(content.name)}
27
+ description: ${escapeYamlValue(content.description)}
28
+ category: ${escapeYamlValue(content.category)}
29
+ tags: ${formatTagsArray(content.tags)}
30
+ ---
31
+
32
+ ${content.body}
33
+ `;
34
+ },
35
+ };
36
+ //# sourceMappingURL=devin.js.map
@@ -4,6 +4,7 @@
4
4
  * Formats commands for Factory Droid following its frontmatter specification.
5
5
  */
6
6
  import path from 'path';
7
+ import { escapeYamlValue } from '../yaml.js';
7
8
  /**
8
9
  * Factory adapter for command generation.
9
10
  * File path: .factory/commands/opsx-<id>.md
@@ -16,7 +17,7 @@ export const factoryAdapter = {
16
17
  },
17
18
  formatFile(content) {
18
19
  return `---
19
- description: ${content.description}
20
+ description: ${escapeYamlValue(content.description)}
20
21
  argument-hint: command arguments
21
22
  ---
22
23