@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.
- package/README.md +3 -1
- package/dist/cli/index.js +6 -1
- package/dist/commands/change.js +15 -1
- package/dist/commands/schema.js +89 -20
- package/dist/commands/spec.js +30 -6
- package/dist/commands/validate.js +2 -2
- package/dist/commands/workflow/instructions.js +32 -27
- package/dist/commands/workflow/status.js +2 -2
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -0
- package/dist/core/archive.js +1265 -151
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
- package/dist/core/artifact-graph/instruction-loader.js +15 -5
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.js +35 -9
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +7 -0
- package/dist/core/change-status-policy.js +2 -2
- package/dist/core/completions/command-registry.js +8 -0
- package/dist/core/config-schema.d.ts +3 -0
- package/dist/core/config-schema.js +28 -1
- package/dist/core/config.d.ts +3 -0
- package/dist/core/config.js +24 -2
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +11 -0
- package/dist/core/init.d.ts +23 -0
- package/dist/core/init.js +234 -39
- package/dist/core/legacy-cleanup.js +4 -2
- package/dist/core/migration.d.ts +6 -4
- package/dist/core/migration.js +78 -22
- package/dist/core/parsers/requirement-blocks.d.ts +10 -0
- package/dist/core/parsers/requirement-blocks.js +60 -0
- package/dist/core/parsers/spec-structure.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +17 -1
- package/dist/core/profile-sync-drift.js +40 -5
- package/dist/core/project-config.d.ts +3 -0
- package/dist/core/project-config.js +31 -1
- package/dist/core/shared/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.js +66 -35
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +72 -0
- package/dist/core/specs-apply.js +448 -55
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +22 -182
- package/dist/core/templates/workflows/archive-change.js +8 -4
- package/dist/core/templates/workflows/bulk-archive-change.js +30 -26
- package/dist/core/templates/workflows/continue-change.js +6 -6
- package/dist/core/templates/workflows/explore.js +42 -18
- package/dist/core/templates/workflows/onboard.js +9 -3
- package/dist/core/templates/workflows/propose.js +72 -20
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +96 -18
- package/dist/core/templates/workflows/update-change.js +10 -8
- package/dist/core/update.d.ts +1 -0
- package/dist/core/update.js +103 -24
- package/dist/core/validation/validator.d.ts +23 -3
- package/dist/core/validation/validator.js +134 -18
- package/dist/core/version-check.js +6 -11
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +9 -4
- package/dist/telemetry/index.js +20 -8
- package/dist/utils/change-metadata.d.ts +17 -4
- package/dist/utils/change-metadata.js +26 -4
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +14 -2
- package/dist/utils/command-references.js +45 -3
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- package/dist/utils/spec-discovery.d.ts +4 -4
- package/dist/utils/spec-discovery.js +19 -6
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +1 -1
- package/schemas/spec-driven/schema.yaml +8 -6
- 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
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
20
|
-
? [FileSystemUtils.canonicalizeExistingPath(
|
|
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, {
|
|
30
|
-
|
|
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 =
|
|
89
|
-
|
|
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 =
|
|
96
|
-
|
|
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 =
|
|
102
|
-
|
|
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:
|
|
23
|
+
generates: relativePathSchema('generates field'),
|
|
6
24
|
description: z.string(),
|
|
7
|
-
template:
|
|
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:
|
|
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
|
|
14
|
-
* the
|
|
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
|
|
16
|
-
* the
|
|
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
|
-
|
|
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(
|
|
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([
|
|
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
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|