@fission-ai/openspec 1.7.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 (95) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/index.js +6 -1
  3. package/dist/commands/change.js +15 -1
  4. package/dist/commands/schema.js +89 -20
  5. package/dist/commands/spec.js +30 -6
  6. package/dist/commands/validate.js +2 -2
  7. package/dist/commands/workflow/instructions.js +32 -27
  8. package/dist/commands/workflow/status.js +2 -2
  9. package/dist/commands/workflow/templates.js +15 -5
  10. package/dist/core/archive.d.ts +14 -0
  11. package/dist/core/archive.js +1265 -151
  12. package/dist/core/artifact-graph/index.d.ts +1 -1
  13. package/dist/core/artifact-graph/index.js +1 -1
  14. package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
  15. package/dist/core/artifact-graph/instruction-loader.js +15 -5
  16. package/dist/core/artifact-graph/outputs.d.ts +1 -0
  17. package/dist/core/artifact-graph/outputs.js +67 -5
  18. package/dist/core/artifact-graph/resolver.js +35 -9
  19. package/dist/core/artifact-graph/types.js +21 -3
  20. package/dist/core/available-tools.d.ts +2 -2
  21. package/dist/core/available-tools.js +14 -3
  22. package/dist/core/change-metadata/schema.d.ts +1 -0
  23. package/dist/core/change-metadata/schema.js +7 -0
  24. package/dist/core/change-status-policy.js +2 -2
  25. package/dist/core/completions/command-registry.js +8 -0
  26. package/dist/core/config-schema.d.ts +3 -0
  27. package/dist/core/config-schema.js +28 -1
  28. package/dist/core/config.d.ts +3 -0
  29. package/dist/core/config.js +24 -2
  30. package/dist/core/file-state.js +56 -24
  31. package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
  32. package/dist/core/github-copilot/cloud-agent.js +497 -0
  33. package/dist/core/global-config.d.ts +11 -0
  34. package/dist/core/init.d.ts +23 -0
  35. package/dist/core/init.js +234 -39
  36. package/dist/core/legacy-cleanup.js +4 -2
  37. package/dist/core/migration.d.ts +6 -4
  38. package/dist/core/migration.js +78 -22
  39. package/dist/core/parsers/requirement-blocks.d.ts +10 -0
  40. package/dist/core/parsers/requirement-blocks.js +60 -0
  41. package/dist/core/parsers/spec-structure.d.ts +1 -1
  42. package/dist/core/parsers/spec-structure.js +17 -1
  43. package/dist/core/profile-sync-drift.js +40 -5
  44. package/dist/core/project-config.d.ts +3 -0
  45. package/dist/core/project-config.js +31 -1
  46. package/dist/core/shared/index.d.ts +1 -0
  47. package/dist/core/shared/index.js +1 -0
  48. package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
  49. package/dist/core/shared/skill-content-equivalence.js +41 -0
  50. package/dist/core/shared/skill-paths.d.ts +13 -0
  51. package/dist/core/shared/skill-paths.js +23 -0
  52. package/dist/core/shared/tool-detection.js +66 -35
  53. package/dist/core/shared-skill-target.d.ts +16 -0
  54. package/dist/core/shared-skill-target.js +156 -0
  55. package/dist/core/specs-apply.d.ts +72 -0
  56. package/dist/core/specs-apply.js +448 -55
  57. package/dist/core/templates/skill-templates.d.ts +1 -1
  58. package/dist/core/templates/skill-templates.js +1 -1
  59. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  60. package/dist/core/templates/workflows/apply-change.js +22 -182
  61. package/dist/core/templates/workflows/archive-change.js +8 -4
  62. package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
  63. package/dist/core/templates/workflows/continue-change.js +6 -6
  64. package/dist/core/templates/workflows/explore.js +42 -18
  65. package/dist/core/templates/workflows/onboard.js +9 -3
  66. package/dist/core/templates/workflows/propose.js +72 -20
  67. package/dist/core/templates/workflows/store-selection.d.ts +1 -1
  68. package/dist/core/templates/workflows/store-selection.js +1 -1
  69. package/dist/core/templates/workflows/sync-specs.js +96 -18
  70. package/dist/core/templates/workflows/update-change.js +10 -8
  71. package/dist/core/update.d.ts +1 -0
  72. package/dist/core/update.js +103 -24
  73. package/dist/core/validation/validator.d.ts +23 -3
  74. package/dist/core/validation/validator.js +134 -18
  75. package/dist/core/version-check.js +6 -11
  76. package/dist/telemetry/config.d.ts +3 -4
  77. package/dist/telemetry/index.d.ts +9 -4
  78. package/dist/telemetry/index.js +20 -8
  79. package/dist/utils/change-metadata.d.ts +17 -4
  80. package/dist/utils/change-metadata.js +26 -4
  81. package/dist/utils/ci.d.ts +12 -0
  82. package/dist/utils/ci.js +16 -0
  83. package/dist/utils/command-references.d.ts +14 -2
  84. package/dist/utils/command-references.js +45 -3
  85. package/dist/utils/file-system.d.ts +10 -0
  86. package/dist/utils/file-system.js +70 -0
  87. package/dist/utils/interactive.d.ts +23 -0
  88. package/dist/utils/interactive.js +32 -0
  89. package/dist/utils/spec-discovery.d.ts +4 -4
  90. package/dist/utils/spec-discovery.js +19 -6
  91. package/dist/utils/task-progress.d.ts +17 -0
  92. package/dist/utils/task-progress.js +41 -13
  93. package/package.json +1 -1
  94. package/schemas/spec-driven/schema.yaml +8 -6
  95. package/schemas/spec-driven/templates/proposal.md +6 -4
@@ -2,7 +2,7 @@ export { ArtifactSchema, SchemaYamlSchema, type Artifact, type SchemaYaml, type
2
2
  export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
3
3
  export { ArtifactGraph } from './graph.js';
4
4
  export { detectCompleted } from './state.js';
5
- export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
5
+ export { artifactOutputExists, isGlobPattern, resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
6
6
  export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, type SchemaInfo, } from './resolver.js';
7
7
  export { loadTemplate, loadChangeContext, generateInstructions, formatChangeStatus, TemplateLoadError, type ChangeContext, type LoadChangeContextOptions, type ArtifactInstructions, type DependencyInfo, type ArtifactStatus, type ChangeStatus, type ArtifactPathSummary, } from './instruction-loader.js';
8
8
  export type { PlanningHomeSummary, ActionContext, } from '../change-status-policy.js';
@@ -6,7 +6,7 @@ export { loadSchema, parseSchema, SchemaValidationError } from './schema.js';
6
6
  export { ArtifactGraph } from './graph.js';
7
7
  // State detection
8
8
  export { detectCompleted } from './state.js';
9
- export { artifactOutputExists, isGlobPattern, resolveArtifactOutputs } from './outputs.js';
9
+ export { artifactOutputExists, isGlobPattern, resolveArtifactOutputPath, resolveArtifactOutputs, } from './outputs.js';
10
10
  // Schema resolution
11
11
  export { resolveSchema, listSchemas, listSchemasWithInfo, getSchemaDir, getPackageSchemasDir, getUserSchemasDir, SchemaLoadError, } from './resolver.js';
12
12
  // Instruction loading
@@ -144,7 +144,9 @@ export interface ChangeStatus {
144
144
  nextSteps: string[];
145
145
  /** Machine-readable action constraints for agents */
146
146
  actionContext: ActionContext;
147
- /** Whether all artifacts are complete */
147
+ /** Whether all planning artifacts are complete */
148
+ isPlanningComplete: boolean;
149
+ /** Compatibility alias for isPlanningComplete */
148
150
  isComplete: boolean;
149
151
  /** Artifact IDs required before apply phase (from schema's apply.requires) */
150
152
  applyRequires: string[];
@@ -3,7 +3,7 @@ import * as path from 'node:path';
3
3
  import { getSchemaDir, resolveSchema, listSchemasWithInfo } from './resolver.js';
4
4
  import { ArtifactGraph } from './graph.js';
5
5
  import { detectCompleted } from './state.js';
6
- import { resolveArtifactOutputs } from './outputs.js';
6
+ import { resolveArtifactOutputPath, resolveArtifactOutputs } from './outputs.js';
7
7
  import { readChangeMetadata, resolveSchemaForChange } from '../../utils/change-metadata.js';
8
8
  import { FileSystemUtils } from '../../utils/file-system.js';
9
9
  import { buildActionContext, buildNextSteps, summarizePlanningHome, } from '../change-status-policy.js';
@@ -42,7 +42,14 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
42
42
  if (!schemaDir) {
43
43
  throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
44
44
  }
45
- const templatePathOnDisk = path.join(schemaDir, 'templates', templatePath);
45
+ const templatesDir = path.join(schemaDir, 'templates');
46
+ const templatePathOnDisk = path.join(templatesDir, templatePath);
47
+ try {
48
+ FileSystemUtils.assertPathWithin(templatesDir, templatePathOnDisk);
49
+ }
50
+ catch (error) {
51
+ throw new TemplateLoadError(error instanceof Error ? error.message : String(error), templatePathOnDisk);
52
+ }
46
53
  if (!fs.existsSync(templatePathOnDisk)) {
47
54
  throw new TemplateLoadError(`Template not found: ${templatePathOnDisk}`, templatePathOnDisk);
48
55
  }
@@ -144,7 +151,9 @@ export function generateInstructions(context, artifactId, projectRoot, options =
144
151
  }
145
152
  // Extract context and rules as separate fields (not prepended to template)
146
153
  const configContext = projectConfig?.context?.trim() || undefined;
147
- const rulesForArtifact = projectConfig?.rules?.[artifactId];
154
+ const rulesForArtifact = projectConfig?.rules && Object.hasOwn(projectConfig.rules, artifactId)
155
+ ? projectConfig.rules[artifactId]
156
+ : undefined;
148
157
  const configRules = rulesForArtifact && rulesForArtifact.length > 0 ? rulesForArtifact : undefined;
149
158
  return {
150
159
  changeName: context.changeName,
@@ -153,7 +162,7 @@ export function generateInstructions(context, artifactId, projectRoot, options =
153
162
  changeDir: context.changeDir,
154
163
  planningHome: summarizePlanningHome(context.planningHome),
155
164
  outputPath: artifact.generates,
156
- resolvedOutputPath: path.join(context.changeDir, artifact.generates),
165
+ resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
157
166
  existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
158
167
  description: artifact.description,
159
168
  instruction: artifact.instruction,
@@ -216,7 +225,7 @@ export function formatChangeStatus(context, options = {}) {
216
225
  const artifactStatuses = artifacts.map(artifact => {
217
226
  artifactPaths[artifact.id] = {
218
227
  outputPath: artifact.generates,
219
- resolvedOutputPath: path.join(context.changeDir, artifact.generates),
228
+ resolvedOutputPath: resolveArtifactOutputPath(context.changeDir, artifact.generates),
220
229
  existingOutputPaths: resolveArtifactOutputs(context.changeDir, artifact.generates),
221
230
  };
222
231
  if (context.skippedArtifacts?.has(artifact.id)) {
@@ -263,6 +272,7 @@ export function formatChangeStatus(context, options = {}) {
263
272
  planningHome: summarizePlanningHome(context.planningHome),
264
273
  changeRoot: context.changeDir,
265
274
  artifactPaths,
275
+ isPlanningComplete: isComplete,
266
276
  isComplete,
267
277
  applyRequires,
268
278
  nextSteps: buildNextSteps({
@@ -2,6 +2,7 @@
2
2
  * Checks if a path contains glob pattern characters.
3
3
  */
4
4
  export declare function isGlobPattern(pattern: string): boolean;
5
+ export declare function resolveArtifactOutputPath(changeDir: string, generates: string): string;
5
6
  /**
6
7
  * Resolves an artifact's output path(s) to concrete files that currently exist.
7
8
  * Returns absolute file paths. Glob matches are sorted for deterministic output.
@@ -8,16 +8,66 @@ import { FileSystemUtils } from '../../utils/file-system.js';
8
8
  export function isGlobPattern(pattern) {
9
9
  return pattern.includes('*') || pattern.includes('?') || pattern.includes('[');
10
10
  }
11
+ export function resolveArtifactOutputPath(changeDir, generates) {
12
+ const outputPath = path.join(changeDir, generates);
13
+ FileSystemUtils.assertPathWithin(changeDir, outputPath);
14
+ return outputPath;
15
+ }
16
+ function assertGlobDirectoryTraversal(changeDir, currentDir, directorySegments, segmentIndex = 0, visited = new Set(), canonicalChangeDir = FileSystemUtils.canonicalizeExistingPath(changeDir), ancestors = new Set()) {
17
+ if (segmentIndex >= directorySegments.length)
18
+ return;
19
+ const canonicalDir = FileSystemUtils.canonicalizeExistingPath(currentDir);
20
+ FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalDir);
21
+ const visitKey = `${canonicalDir}\0${segmentIndex}`;
22
+ if (ancestors.has(visitKey)) {
23
+ throw new Error(`Cannot resolve artifact outputs through a linked directory cycle: ${currentDir}`);
24
+ }
25
+ if (visited.has(visitKey))
26
+ return;
27
+ visited.add(visitKey);
28
+ ancestors.add(visitKey);
29
+ try {
30
+ const segment = directorySegments[segmentIndex];
31
+ if (segment === '**') {
32
+ // `**` may consume no directory at all.
33
+ assertGlobDirectoryTraversal(changeDir, canonicalDir, directorySegments, segmentIndex + 1, visited, canonicalChangeDir, ancestors);
34
+ }
35
+ const matches = fg.sync(segment === '**' ? '*' : segment, {
36
+ cwd: canonicalDir,
37
+ onlyFiles: false,
38
+ followSymbolicLinks: false,
39
+ deep: 1,
40
+ });
41
+ for (const match of matches) {
42
+ const candidate = path.join(canonicalDir, match);
43
+ try {
44
+ if (!fs.statSync(candidate).isDirectory())
45
+ continue;
46
+ }
47
+ catch (error) {
48
+ if (error.code === 'ENOENT')
49
+ continue;
50
+ throw error;
51
+ }
52
+ const canonicalCandidate = FileSystemUtils.canonicalizeExistingPath(candidate);
53
+ FileSystemUtils.assertPathWithin(canonicalChangeDir, canonicalCandidate);
54
+ assertGlobDirectoryTraversal(changeDir, canonicalCandidate, directorySegments, segment === '**' ? segmentIndex : segmentIndex + 1, visited, canonicalChangeDir, ancestors);
55
+ }
56
+ }
57
+ finally {
58
+ ancestors.delete(visitKey);
59
+ }
60
+ }
11
61
  /**
12
62
  * Resolves an artifact's output path(s) to concrete files that currently exist.
13
63
  * Returns absolute file paths. Glob matches are sorted for deterministic output.
14
64
  */
15
65
  export function resolveArtifactOutputs(changeDir, generates) {
66
+ const outputPath = resolveArtifactOutputPath(changeDir, generates);
16
67
  if (!isGlobPattern(generates)) {
17
- const fullPath = path.join(changeDir, generates);
18
68
  try {
19
- return fs.statSync(fullPath).isFile()
20
- ? [FileSystemUtils.canonicalizeExistingPath(fullPath)]
69
+ return fs.statSync(outputPath).isFile()
70
+ ? [FileSystemUtils.canonicalizeExistingPath(outputPath)]
21
71
  : [];
22
72
  }
23
73
  catch {
@@ -25,9 +75,21 @@ export function resolveArtifactOutputs(changeDir, generates) {
25
75
  }
26
76
  }
27
77
  const normalizedPattern = FileSystemUtils.toPosixPath(generates);
78
+ assertGlobDirectoryTraversal(changeDir, changeDir, normalizedPattern.split('/').slice(0, -1));
28
79
  const matches = fg
29
- .sync(normalizedPattern, { cwd: changeDir, onlyFiles: true, absolute: true })
30
- .map((match) => FileSystemUtils.canonicalizeExistingPath(path.normalize(match)));
80
+ .sync(normalizedPattern, {
81
+ cwd: changeDir,
82
+ onlyFiles: true,
83
+ absolute: true,
84
+ // Preserve existing support for linked artifact directories. Every
85
+ // concrete match is canonically confined below before it is returned.
86
+ followSymbolicLinks: true,
87
+ })
88
+ .map((match) => {
89
+ const normalizedMatch = path.normalize(match);
90
+ FileSystemUtils.assertPathWithin(changeDir, normalizedMatch);
91
+ return FileSystemUtils.canonicalizeExistingPath(normalizedMatch);
92
+ });
31
93
  return Array.from(new Set(matches)).sort();
32
94
  }
33
95
  /**
@@ -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.
@@ -67,6 +68,25 @@ export function isSchemaDir(parentDir, entry) {
67
68
  }
68
69
  return false;
69
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
+ }
70
90
  /**
71
91
  * Resolves a schema name to its directory path.
72
92
  *
@@ -83,24 +103,30 @@ export function isSchemaDir(parentDir, entry) {
83
103
  * @returns The path to the schema directory, or null if not found
84
104
  */
85
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
+ }
86
115
  // 1. Check project-local directory (if projectRoot provided)
87
116
  if (projectRoot) {
88
- const projectDir = path.join(getProjectSchemasDir(projectRoot), name);
89
- const projectSchemaPath = path.join(projectDir, 'schema.yaml');
90
- if (fs.existsSync(projectSchemaPath)) {
117
+ const projectDir = getSchemaCandidateDir(getProjectSchemasDir(projectRoot), name);
118
+ if (projectDir) {
91
119
  return projectDir;
92
120
  }
93
121
  }
94
122
  // 2. Check user override directory
95
- const userDir = path.join(getUserSchemasDir(), name);
96
- const userSchemaPath = path.join(userDir, 'schema.yaml');
97
- if (fs.existsSync(userSchemaPath)) {
123
+ const userDir = getSchemaCandidateDir(getUserSchemasDir(), name);
124
+ if (userDir) {
98
125
  return userDir;
99
126
  }
100
127
  // 3. Check package built-in directory
101
- const packageDir = path.join(getPackageSchemasDir(), name);
102
- const packageSchemaPath = path.join(packageDir, 'schema.yaml');
103
- if (fs.existsSync(packageSchemaPath)) {
128
+ const packageDir = getSchemaCandidateDir(getPackageSchemasDir(), name);
129
+ if (packageDir) {
104
130
  return packageDir;
105
131
  }
106
132
  return null;
@@ -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
@@ -15,6 +15,7 @@ export declare const ChangeMetadataSchema: z.ZodObject<{
15
15
  id: z.ZodString;
16
16
  }, z.core.$strict>>;
17
17
  skip_specs: z.ZodOptional<z.ZodBoolean>;
18
+ retire_capabilities: z.ZodOptional<z.ZodBoolean>;
18
19
  }, z.core.$strip>;
19
20
  export type ChangeMetadata = z.infer<typeof ChangeMetadataSchema>;
20
21
  //# sourceMappingURL=schema.d.ts.map
@@ -32,5 +32,12 @@ export const ChangeMetadataSchema = z.object({
32
32
  // complete - that path prefix, not the artifact id, is the contract custom
33
33
  // schemas inherit.
34
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(),
35
42
  });
36
43
  //# sourceMappingURL=schema.js.map
@@ -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
  }
@@ -26,6 +26,14 @@ export const COMMAND_REGISTRY = [
26
26
  name: 'no-animation',
27
27
  description: 'Show a static welcome screen instead of the animated one',
28
28
  },
29
+ {
30
+ name: 'copilot-cloud',
31
+ description: 'Generate GitHub Copilot cloud coding-agent files (opt-in; default: prompt)',
32
+ },
33
+ {
34
+ name: 'no-copilot-cloud',
35
+ description: 'Skip generating GitHub Copilot cloud coding-agent files',
36
+ },
29
37
  ],
30
38
  },
31
39
  {
@@ -16,6 +16,9 @@ export declare const GlobalConfigSchema: z.ZodObject<{
16
16
  }>>>;
17
17
  workflows: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
18
  defaultStore: z.ZodOptional<z.ZodString>;
19
+ telemetry: z.ZodOptional<z.ZodObject<{
20
+ enabled: z.ZodOptional<z.ZodBoolean>;
21
+ }, z.core.$loose>>;
19
22
  }, z.core.$loose>;
20
23
  export type GlobalConfigType = z.infer<typeof GlobalConfigSchema>;
21
24
  /**
@@ -24,6 +24,14 @@ export const GlobalConfigSchema = z
24
24
  .string()
25
25
  .optional()
26
26
  .describe('Store id used as fallback root when no explicit --store, local root, or project-level store: pointer resolves'),
27
+ // passthrough keeps runtime-managed fields (anonymousId, noticeSeen) valid
28
+ // under CLI validate when users only set telemetry.enabled.
29
+ telemetry: z
30
+ .object({
31
+ enabled: z.boolean().optional(),
32
+ })
33
+ .passthrough()
34
+ .optional(),
27
35
  })
28
36
  .passthrough();
29
37
  /**
@@ -34,7 +42,14 @@ export const DEFAULT_CONFIG = {
34
42
  profile: 'core',
35
43
  delivery: 'both',
36
44
  };
37
- const KNOWN_TOP_LEVEL_KEYS = new Set([...Object.keys(DEFAULT_CONFIG), 'workflows', 'defaultStore']);
45
+ const KNOWN_TOP_LEVEL_KEYS = new Set([
46
+ ...Object.keys(DEFAULT_CONFIG),
47
+ 'workflows',
48
+ 'defaultStore',
49
+ 'telemetry',
50
+ ]);
51
+ /** Nested keys users may set under `telemetry` via the CLI. */
52
+ const TELEMETRY_SETTABLE_KEYS = new Set(['enabled']);
38
53
  /**
39
54
  * Key segments that would reach the prototype chain instead of the config object.
40
55
  * Never valid as configuration keys, so rejecting them costs nothing.
@@ -73,6 +88,18 @@ export function validateConfigKeyPath(path) {
73
88
  }
74
89
  return { valid: true };
75
90
  }
91
+ if (rootKey === 'telemetry') {
92
+ if (rawKeys.length === 1) {
93
+ return { valid: false, reason: 'Set nested keys under telemetry (e.g. telemetry.enabled)' };
94
+ }
95
+ if (rawKeys.length !== 2 || !TELEMETRY_SETTABLE_KEYS.has(rawKeys[1])) {
96
+ return {
97
+ valid: false,
98
+ reason: `Unknown telemetry key "${rawKeys.slice(1).join('.')}" (allowed: enabled)`,
99
+ };
100
+ }
101
+ return { valid: true };
102
+ }
76
103
  if (rawKeys.length > 1) {
77
104
  return { valid: false, reason: `"${rootKey}" does not support nested keys` };
78
105
  }
@@ -1,4 +1,5 @@
1
1
  export declare const OPENSPEC_DIR_NAME = "openspec";
2
+ export declare const OPENSPEC_SKILL_NAMES: readonly ["openspec-explore", "openspec-new-change", "openspec-continue-change", "openspec-apply-change", "openspec-update-change", "openspec-ff-change", "openspec-sync-specs", "openspec-archive-change", "openspec-bulk-archive-change", "openspec-verify-change", "openspec-onboard", "openspec-propose"];
2
3
  export declare const OPENSPEC_MARKERS: {
3
4
  start: string;
4
5
  end: string;
@@ -12,6 +13,8 @@ export interface AIToolOption {
12
13
  available: boolean;
13
14
  successLabel?: string;
14
15
  skillsDir?: string;
16
+ legacySkillsDirs?: string[];
17
+ globalSkillsDir?: string;
15
18
  detectionPaths?: string[];
16
19
  setupNote?: string;
17
20
  }
@@ -1,4 +1,18 @@
1
1
  export const OPENSPEC_DIR_NAME = 'openspec';
2
+ export const OPENSPEC_SKILL_NAMES = [
3
+ 'openspec-explore',
4
+ 'openspec-new-change',
5
+ 'openspec-continue-change',
6
+ 'openspec-apply-change',
7
+ 'openspec-update-change',
8
+ 'openspec-ff-change',
9
+ 'openspec-sync-specs',
10
+ 'openspec-archive-change',
11
+ 'openspec-bulk-archive-change',
12
+ 'openspec-verify-change',
13
+ 'openspec-onboard',
14
+ 'openspec-propose',
15
+ ];
2
16
  export const OPENSPEC_MARKERS = {
3
17
  start: '<!-- OPENSPEC:START -->',
4
18
  end: '<!-- OPENSPEC:END -->'
@@ -11,7 +25,7 @@ export const AI_TOOLS = [
11
25
  { name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
12
26
  { name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
13
27
  { name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' },
14
- { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },
28
+ { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] },
15
29
  { name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'] },
16
30
  { name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
17
31
  { name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
@@ -29,16 +43,24 @@ export const AI_TOOLS = [
29
43
  { name: 'Kimi Code', value: 'kimi', available: true, successLabel: 'Kimi Code', skillsDir: '.kimi-code', detectionPaths: ['.kimi-code', '.kimi'] },
30
44
  { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
31
45
  { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
46
+ { name: 'MiniMax Code', value: 'minimax-code', available: true, successLabel: 'MiniMax Code', globalSkillsDir: '.minimax' },
32
47
  { name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' },
33
48
  { name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
34
49
  { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
35
50
  { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
36
51
  { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
37
52
  { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
53
+ { name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
38
54
  { name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo' },
39
55
  { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
40
56
  { name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' },
41
- { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
57
+ // Vendor-neutral target for assistants that read the shared `.agents` root.
58
+ // Detection keys off `.agents/skills` rather than the bare root: frameworks use
59
+ // `.agents/` for more than skills, so the root alone says nothing about skills.
60
+ // A project that does keep skills there is a project this target fits, the same
61
+ // way `.claude/` selects Claude Code — the signal is the user's setup, not
62
+ // OpenSpec's own files.
63
+ { name: 'Shared .agents skills', value: 'agents', available: true, successLabel: 'shared .agents skills', skillsDir: '.agents', detectionPaths: ['.agents/skills'] }
42
64
  ];
43
65
  /**
44
66
  * Retired tool ids that still resolve, so a rebrand does not break scripted