@fission-ai/openspec 0.17.2 → 0.18.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 (52) hide show
  1. package/dist/cli/index.js +7 -1
  2. package/dist/commands/artifact-workflow.d.ts +17 -0
  3. package/dist/commands/artifact-workflow.js +818 -0
  4. package/dist/core/archive.d.ts +0 -5
  5. package/dist/core/archive.js +4 -257
  6. package/dist/core/artifact-graph/graph.d.ts +56 -0
  7. package/dist/core/artifact-graph/graph.js +141 -0
  8. package/dist/core/artifact-graph/index.d.ts +7 -0
  9. package/dist/core/artifact-graph/index.js +13 -0
  10. package/dist/core/artifact-graph/instruction-loader.d.ts +130 -0
  11. package/dist/core/artifact-graph/instruction-loader.js +173 -0
  12. package/dist/core/artifact-graph/resolver.d.ts +61 -0
  13. package/dist/core/artifact-graph/resolver.js +187 -0
  14. package/dist/core/artifact-graph/schema.d.ts +13 -0
  15. package/dist/core/artifact-graph/schema.js +108 -0
  16. package/dist/core/artifact-graph/state.d.ts +12 -0
  17. package/dist/core/artifact-graph/state.js +54 -0
  18. package/dist/core/artifact-graph/types.d.ts +45 -0
  19. package/dist/core/artifact-graph/types.js +43 -0
  20. package/dist/core/converters/json-converter.js +2 -1
  21. package/dist/core/global-config.d.ts +10 -0
  22. package/dist/core/global-config.js +28 -0
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/index.js +1 -1
  25. package/dist/core/list.d.ts +6 -1
  26. package/dist/core/list.js +88 -6
  27. package/dist/core/specs-apply.d.ts +73 -0
  28. package/dist/core/specs-apply.js +384 -0
  29. package/dist/core/templates/skill-templates.d.ts +76 -0
  30. package/dist/core/templates/skill-templates.js +1472 -0
  31. package/dist/core/update.js +1 -1
  32. package/dist/core/validation/validator.js +2 -1
  33. package/dist/core/view.js +28 -8
  34. package/dist/utils/change-metadata.d.ts +47 -0
  35. package/dist/utils/change-metadata.js +130 -0
  36. package/dist/utils/change-utils.d.ts +51 -0
  37. package/dist/utils/change-utils.js +100 -0
  38. package/dist/utils/file-system.d.ts +5 -0
  39. package/dist/utils/file-system.js +7 -0
  40. package/dist/utils/index.d.ts +3 -1
  41. package/dist/utils/index.js +4 -1
  42. package/package.json +4 -1
  43. package/schemas/spec-driven/schema.yaml +148 -0
  44. package/schemas/spec-driven/templates/design.md +19 -0
  45. package/schemas/spec-driven/templates/proposal.md +23 -0
  46. package/schemas/spec-driven/templates/spec.md +8 -0
  47. package/schemas/spec-driven/templates/tasks.md +9 -0
  48. package/schemas/tdd/schema.yaml +213 -0
  49. package/schemas/tdd/templates/docs.md +15 -0
  50. package/schemas/tdd/templates/implementation.md +11 -0
  51. package/schemas/tdd/templates/spec.md +11 -0
  52. package/schemas/tdd/templates/test.md +11 -0
@@ -71,7 +71,7 @@ export class UpdateCommand {
71
71
  }
72
72
  if (updatedSlashFiles.length > 0) {
73
73
  // Normalize to forward slashes for cross-platform log consistency
74
- const normalized = updatedSlashFiles.map((p) => p.replace(/\\/g, '/'));
74
+ const normalized = updatedSlashFiles.map((p) => FileSystemUtils.toPosixPath(p));
75
75
  summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
76
76
  }
77
77
  const failedItems = [
@@ -5,6 +5,7 @@ import { MarkdownParser } from '../parsers/markdown-parser.js';
5
5
  import { ChangeParser } from '../parsers/change-parser.js';
6
6
  import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
7
7
  import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
8
+ import { FileSystemUtils } from '../../utils/file-system.js';
8
9
  export class Validator {
9
10
  strictMode;
10
11
  constructor(strictMode = false) {
@@ -330,7 +331,7 @@ export class Validator {
330
331
  return msg;
331
332
  }
332
333
  extractNameFromPath(filePath) {
333
- const normalizedPath = filePath.replaceAll('\\', '/');
334
+ const normalizedPath = FileSystemUtils.toPosixPath(filePath);
334
335
  const parts = normalizedPath.split('/');
335
336
  // Look for the directory name after 'specs' or 'changes'
336
337
  for (let i = parts.length - 1; i >= 0; i--) {
package/dist/core/view.js CHANGED
@@ -17,11 +17,19 @@ export class ViewCommand {
17
17
  const specsData = await this.getSpecsData(openspecDir);
18
18
  // Display summary metrics
19
19
  this.displaySummary(changesData, specsData);
20
+ // Display draft changes
21
+ if (changesData.draft.length > 0) {
22
+ console.log(chalk.bold.gray('\nDraft Changes'));
23
+ console.log('─'.repeat(60));
24
+ changesData.draft.forEach((change) => {
25
+ console.log(` ${chalk.gray('○')} ${change.name}`);
26
+ });
27
+ }
20
28
  // Display active changes
21
29
  if (changesData.active.length > 0) {
22
30
  console.log(chalk.bold.cyan('\nActive Changes'));
23
31
  console.log('─'.repeat(60));
24
- changesData.active.forEach(change => {
32
+ changesData.active.forEach((change) => {
25
33
  const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
26
34
  const percentage = change.progress.total > 0
27
35
  ? Math.round((change.progress.completed / change.progress.total) * 100)
@@ -33,7 +41,7 @@ export class ViewCommand {
33
41
  if (changesData.completed.length > 0) {
34
42
  console.log(chalk.bold.green('\nCompleted Changes'));
35
43
  console.log('─'.repeat(60));
36
- changesData.completed.forEach(change => {
44
+ changesData.completed.forEach((change) => {
37
45
  console.log(` ${chalk.green('✓')} ${change.name}`);
38
46
  });
39
47
  }
@@ -54,23 +62,32 @@ export class ViewCommand {
54
62
  async getChangesData(openspecDir) {
55
63
  const changesDir = path.join(openspecDir, 'changes');
56
64
  if (!fs.existsSync(changesDir)) {
57
- return { active: [], completed: [] };
65
+ return { draft: [], active: [], completed: [] };
58
66
  }
67
+ const draft = [];
59
68
  const active = [];
60
69
  const completed = [];
61
70
  const entries = fs.readdirSync(changesDir, { withFileTypes: true });
62
71
  for (const entry of entries) {
63
72
  if (entry.isDirectory() && entry.name !== 'archive') {
64
73
  const progress = await getTaskProgressForChange(changesDir, entry.name);
65
- if (progress.total === 0 || progress.completed === progress.total) {
74
+ if (progress.total === 0) {
75
+ // No tasks defined yet - still in planning/draft phase
76
+ draft.push({ name: entry.name });
77
+ }
78
+ else if (progress.completed === progress.total) {
79
+ // All tasks complete
66
80
  completed.push({ name: entry.name });
67
81
  }
68
82
  else {
83
+ // Has tasks but not all complete
69
84
  active.push({ name: entry.name, progress });
70
85
  }
71
86
  }
72
87
  }
73
- // Sort active changes by completion percentage (ascending) and then by name for deterministic ordering
88
+ // Sort all categories by name for deterministic ordering
89
+ draft.sort((a, b) => a.name.localeCompare(b.name));
90
+ // Sort active changes by completion percentage (ascending) and then by name
74
91
  active.sort((a, b) => {
75
92
  const percentageA = a.progress.total > 0 ? a.progress.completed / a.progress.total : 0;
76
93
  const percentageB = b.progress.total > 0 ? b.progress.completed / b.progress.total : 0;
@@ -81,7 +98,7 @@ export class ViewCommand {
81
98
  return a.name.localeCompare(b.name);
82
99
  });
83
100
  completed.sort((a, b) => a.name.localeCompare(b.name));
84
- return { active, completed };
101
+ return { draft, active, completed };
85
102
  }
86
103
  async getSpecsData(openspecDir) {
87
104
  const specsDir = path.join(openspecDir, 'specs');
@@ -111,13 +128,13 @@ export class ViewCommand {
111
128
  return specs;
112
129
  }
113
130
  displaySummary(changesData, specsData) {
114
- const totalChanges = changesData.active.length + changesData.completed.length;
131
+ const totalChanges = changesData.draft.length + changesData.active.length + changesData.completed.length;
115
132
  const totalSpecs = specsData.length;
116
133
  const totalRequirements = specsData.reduce((sum, spec) => sum + spec.requirementCount, 0);
117
134
  // Calculate total task progress
118
135
  let totalTasks = 0;
119
136
  let completedTasks = 0;
120
- changesData.active.forEach(change => {
137
+ changesData.active.forEach((change) => {
121
138
  totalTasks += change.progress.total;
122
139
  completedTasks += change.progress.completed;
123
140
  });
@@ -127,6 +144,9 @@ export class ViewCommand {
127
144
  });
128
145
  console.log(chalk.bold('Summary:'));
129
146
  console.log(` ${chalk.cyan('●')} Specifications: ${chalk.bold(totalSpecs)} specs, ${chalk.bold(totalRequirements)} requirements`);
147
+ if (changesData.draft.length > 0) {
148
+ console.log(` ${chalk.gray('●')} Draft Changes: ${chalk.bold(changesData.draft.length)}`);
149
+ }
130
150
  console.log(` ${chalk.yellow('●')} Active Changes: ${chalk.bold(changesData.active.length)} in progress`);
131
151
  console.log(` ${chalk.green('●')} Completed Changes: ${chalk.bold(changesData.completed.length)}`);
132
152
  if (totalTasks > 0) {
@@ -0,0 +1,47 @@
1
+ import { type ChangeMetadata } from '../core/artifact-graph/types.js';
2
+ /**
3
+ * Error thrown when change metadata validation fails.
4
+ */
5
+ export declare class ChangeMetadataError extends Error {
6
+ readonly metadataPath: string;
7
+ readonly cause?: Error | undefined;
8
+ constructor(message: string, metadataPath: string, cause?: Error | undefined);
9
+ }
10
+ /**
11
+ * Validates that a schema name is valid (exists in available schemas).
12
+ *
13
+ * @param schemaName - The schema name to validate
14
+ * @returns The validated schema name
15
+ * @throws Error if schema is not found
16
+ */
17
+ export declare function validateSchemaName(schemaName: string): string;
18
+ /**
19
+ * Writes change metadata to .openspec.yaml in the change directory.
20
+ *
21
+ * @param changeDir - The path to the change directory
22
+ * @param metadata - The metadata to write
23
+ * @throws ChangeMetadataError if validation fails or write fails
24
+ */
25
+ export declare function writeChangeMetadata(changeDir: string, metadata: ChangeMetadata): void;
26
+ /**
27
+ * Reads change metadata from .openspec.yaml in the change directory.
28
+ *
29
+ * @param changeDir - The path to the change directory
30
+ * @returns The validated metadata, or null if no metadata file exists
31
+ * @throws ChangeMetadataError if the file exists but is invalid
32
+ */
33
+ export declare function readChangeMetadata(changeDir: string): ChangeMetadata | null;
34
+ /**
35
+ * Resolves the schema for a change, with explicit override taking precedence.
36
+ *
37
+ * Resolution order:
38
+ * 1. Explicit schema (if provided)
39
+ * 2. Schema from .openspec.yaml metadata (if exists)
40
+ * 3. Default 'spec-driven'
41
+ *
42
+ * @param changeDir - The path to the change directory
43
+ * @param explicitSchema - Optional explicit schema override
44
+ * @returns The resolved schema name
45
+ */
46
+ export declare function resolveSchemaForChange(changeDir: string, explicitSchema?: string): string;
47
+ //# sourceMappingURL=change-metadata.d.ts.map
@@ -0,0 +1,130 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as yaml from 'yaml';
4
+ import { ChangeMetadataSchema } from '../core/artifact-graph/types.js';
5
+ import { listSchemas } from '../core/artifact-graph/resolver.js';
6
+ const METADATA_FILENAME = '.openspec.yaml';
7
+ /**
8
+ * Error thrown when change metadata validation fails.
9
+ */
10
+ export class ChangeMetadataError extends Error {
11
+ metadataPath;
12
+ cause;
13
+ constructor(message, metadataPath, cause) {
14
+ super(message);
15
+ this.metadataPath = metadataPath;
16
+ this.cause = cause;
17
+ this.name = 'ChangeMetadataError';
18
+ }
19
+ }
20
+ /**
21
+ * Validates that a schema name is valid (exists in available schemas).
22
+ *
23
+ * @param schemaName - The schema name to validate
24
+ * @returns The validated schema name
25
+ * @throws Error if schema is not found
26
+ */
27
+ export function validateSchemaName(schemaName) {
28
+ const availableSchemas = listSchemas();
29
+ if (!availableSchemas.includes(schemaName)) {
30
+ throw new Error(`Unknown schema '${schemaName}'. Available: ${availableSchemas.join(', ')}`);
31
+ }
32
+ return schemaName;
33
+ }
34
+ /**
35
+ * Writes change metadata to .openspec.yaml in the change directory.
36
+ *
37
+ * @param changeDir - The path to the change directory
38
+ * @param metadata - The metadata to write
39
+ * @throws ChangeMetadataError if validation fails or write fails
40
+ */
41
+ export function writeChangeMetadata(changeDir, metadata) {
42
+ const metaPath = path.join(changeDir, METADATA_FILENAME);
43
+ // Validate schema exists
44
+ validateSchemaName(metadata.schema);
45
+ // Validate with Zod
46
+ const parseResult = ChangeMetadataSchema.safeParse(metadata);
47
+ if (!parseResult.success) {
48
+ throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath);
49
+ }
50
+ // Write YAML file
51
+ const content = yaml.stringify(parseResult.data);
52
+ try {
53
+ fs.writeFileSync(metaPath, content, 'utf-8');
54
+ }
55
+ catch (err) {
56
+ const ioError = err instanceof Error ? err : new Error(String(err));
57
+ throw new ChangeMetadataError(`Failed to write metadata: ${ioError.message}`, metaPath, ioError);
58
+ }
59
+ }
60
+ /**
61
+ * Reads change metadata from .openspec.yaml in the change directory.
62
+ *
63
+ * @param changeDir - The path to the change directory
64
+ * @returns The validated metadata, or null if no metadata file exists
65
+ * @throws ChangeMetadataError if the file exists but is invalid
66
+ */
67
+ export function readChangeMetadata(changeDir) {
68
+ const metaPath = path.join(changeDir, METADATA_FILENAME);
69
+ if (!fs.existsSync(metaPath)) {
70
+ return null;
71
+ }
72
+ let content;
73
+ try {
74
+ content = fs.readFileSync(metaPath, 'utf-8');
75
+ }
76
+ catch (err) {
77
+ const ioError = err instanceof Error ? err : new Error(String(err));
78
+ throw new ChangeMetadataError(`Failed to read metadata: ${ioError.message}`, metaPath, ioError);
79
+ }
80
+ let parsed;
81
+ try {
82
+ parsed = yaml.parse(content);
83
+ }
84
+ catch (err) {
85
+ const parseError = err instanceof Error ? err : new Error(String(err));
86
+ throw new ChangeMetadataError(`Invalid YAML in metadata file: ${parseError.message}`, metaPath, parseError);
87
+ }
88
+ // Validate with Zod
89
+ const parseResult = ChangeMetadataSchema.safeParse(parsed);
90
+ if (!parseResult.success) {
91
+ throw new ChangeMetadataError(`Invalid metadata: ${parseResult.error.message}`, metaPath);
92
+ }
93
+ // Validate that the schema exists
94
+ const availableSchemas = listSchemas();
95
+ if (!availableSchemas.includes(parseResult.data.schema)) {
96
+ throw new ChangeMetadataError(`Unknown schema '${parseResult.data.schema}'. Available: ${availableSchemas.join(', ')}`, metaPath);
97
+ }
98
+ return parseResult.data;
99
+ }
100
+ /**
101
+ * Resolves the schema for a change, with explicit override taking precedence.
102
+ *
103
+ * Resolution order:
104
+ * 1. Explicit schema (if provided)
105
+ * 2. Schema from .openspec.yaml metadata (if exists)
106
+ * 3. Default 'spec-driven'
107
+ *
108
+ * @param changeDir - The path to the change directory
109
+ * @param explicitSchema - Optional explicit schema override
110
+ * @returns The resolved schema name
111
+ */
112
+ export function resolveSchemaForChange(changeDir, explicitSchema) {
113
+ // 1. Explicit override wins
114
+ if (explicitSchema) {
115
+ return explicitSchema;
116
+ }
117
+ // 2. Try reading from metadata
118
+ try {
119
+ const metadata = readChangeMetadata(changeDir);
120
+ if (metadata?.schema) {
121
+ return metadata.schema;
122
+ }
123
+ }
124
+ catch {
125
+ // If metadata read fails, fall back to default
126
+ }
127
+ // 3. Default
128
+ return 'spec-driven';
129
+ }
130
+ //# sourceMappingURL=change-metadata.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Options for creating a change.
3
+ */
4
+ export interface CreateChangeOptions {
5
+ /** The workflow schema to use (default: 'spec-driven') */
6
+ schema?: string;
7
+ }
8
+ /**
9
+ * Result of validating a change name.
10
+ */
11
+ export interface ValidationResult {
12
+ valid: boolean;
13
+ error?: string;
14
+ }
15
+ /**
16
+ * Validates that a change name follows kebab-case conventions.
17
+ *
18
+ * Valid names:
19
+ * - Start with a lowercase letter
20
+ * - Contain only lowercase letters, numbers, and hyphens
21
+ * - Do not start or end with a hyphen
22
+ * - Do not contain consecutive hyphens
23
+ *
24
+ * @param name - The change name to validate
25
+ * @returns Validation result with `valid: true` or `valid: false` with an error message
26
+ *
27
+ * @example
28
+ * validateChangeName('add-auth') // { valid: true }
29
+ * validateChangeName('Add-Auth') // { valid: false, error: '...' }
30
+ */
31
+ export declare function validateChangeName(name: string): ValidationResult;
32
+ /**
33
+ * Creates a new change directory with metadata file.
34
+ *
35
+ * @param projectRoot - The root directory of the project (where `openspec/` lives)
36
+ * @param name - The change name (must be valid kebab-case)
37
+ * @param options - Optional settings for the change
38
+ * @throws Error if the change name is invalid
39
+ * @throws Error if the schema name is invalid
40
+ * @throws Error if the change directory already exists
41
+ *
42
+ * @example
43
+ * // Creates openspec/changes/add-auth/ with default schema
44
+ * await createChange('/path/to/project', 'add-auth')
45
+ *
46
+ * @example
47
+ * // Creates openspec/changes/add-auth/ with TDD schema
48
+ * await createChange('/path/to/project', 'add-auth', { schema: 'tdd' })
49
+ */
50
+ export declare function createChange(projectRoot: string, name: string, options?: CreateChangeOptions): Promise<void>;
51
+ //# sourceMappingURL=change-utils.d.ts.map
@@ -0,0 +1,100 @@
1
+ import path from 'path';
2
+ import { FileSystemUtils } from './file-system.js';
3
+ import { writeChangeMetadata, validateSchemaName } from './change-metadata.js';
4
+ const DEFAULT_SCHEMA = 'spec-driven';
5
+ /**
6
+ * Validates that a change name follows kebab-case conventions.
7
+ *
8
+ * Valid names:
9
+ * - Start with a lowercase letter
10
+ * - Contain only lowercase letters, numbers, and hyphens
11
+ * - Do not start or end with a hyphen
12
+ * - Do not contain consecutive hyphens
13
+ *
14
+ * @param name - The change name to validate
15
+ * @returns Validation result with `valid: true` or `valid: false` with an error message
16
+ *
17
+ * @example
18
+ * validateChangeName('add-auth') // { valid: true }
19
+ * validateChangeName('Add-Auth') // { valid: false, error: '...' }
20
+ */
21
+ export function validateChangeName(name) {
22
+ // Pattern: starts with lowercase letter, followed by lowercase letters/numbers,
23
+ // optionally followed by hyphen + lowercase letters/numbers (repeatable)
24
+ const kebabCasePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
25
+ if (!name) {
26
+ return { valid: false, error: 'Change name cannot be empty' };
27
+ }
28
+ if (!kebabCasePattern.test(name)) {
29
+ // Provide specific error messages for common mistakes
30
+ if (/[A-Z]/.test(name)) {
31
+ return { valid: false, error: 'Change name must be lowercase (use kebab-case)' };
32
+ }
33
+ if (/\s/.test(name)) {
34
+ return { valid: false, error: 'Change name cannot contain spaces (use hyphens instead)' };
35
+ }
36
+ if (/_/.test(name)) {
37
+ return { valid: false, error: 'Change name cannot contain underscores (use hyphens instead)' };
38
+ }
39
+ if (name.startsWith('-')) {
40
+ return { valid: false, error: 'Change name cannot start with a hyphen' };
41
+ }
42
+ if (name.endsWith('-')) {
43
+ return { valid: false, error: 'Change name cannot end with a hyphen' };
44
+ }
45
+ if (/--/.test(name)) {
46
+ return { valid: false, error: 'Change name cannot contain consecutive hyphens' };
47
+ }
48
+ if (/[^a-z0-9-]/.test(name)) {
49
+ return { valid: false, error: 'Change name can only contain lowercase letters, numbers, and hyphens' };
50
+ }
51
+ if (/^[0-9]/.test(name)) {
52
+ return { valid: false, error: 'Change name must start with a letter' };
53
+ }
54
+ return { valid: false, error: 'Change name must follow kebab-case convention (e.g., add-auth, refactor-db)' };
55
+ }
56
+ return { valid: true };
57
+ }
58
+ /**
59
+ * Creates a new change directory with metadata file.
60
+ *
61
+ * @param projectRoot - The root directory of the project (where `openspec/` lives)
62
+ * @param name - The change name (must be valid kebab-case)
63
+ * @param options - Optional settings for the change
64
+ * @throws Error if the change name is invalid
65
+ * @throws Error if the schema name is invalid
66
+ * @throws Error if the change directory already exists
67
+ *
68
+ * @example
69
+ * // Creates openspec/changes/add-auth/ with default schema
70
+ * await createChange('/path/to/project', 'add-auth')
71
+ *
72
+ * @example
73
+ * // Creates openspec/changes/add-auth/ with TDD schema
74
+ * await createChange('/path/to/project', 'add-auth', { schema: 'tdd' })
75
+ */
76
+ export async function createChange(projectRoot, name, options = {}) {
77
+ // Validate the name first
78
+ const validation = validateChangeName(name);
79
+ if (!validation.valid) {
80
+ throw new Error(validation.error);
81
+ }
82
+ // Determine schema (validate if provided)
83
+ const schemaName = options.schema ?? DEFAULT_SCHEMA;
84
+ validateSchemaName(schemaName);
85
+ // Build the change directory path
86
+ const changeDir = path.join(projectRoot, 'openspec', 'changes', name);
87
+ // Check if change already exists
88
+ if (await FileSystemUtils.directoryExists(changeDir)) {
89
+ throw new Error(`Change '${name}' already exists at ${changeDir}`);
90
+ }
91
+ // Create the directory (including parent directories if needed)
92
+ await FileSystemUtils.createDirectory(changeDir);
93
+ // Write metadata file with schema and creation date
94
+ const today = new Date().toISOString().split('T')[0];
95
+ writeChangeMetadata(changeDir, {
96
+ schema: schemaName,
97
+ created: today,
98
+ });
99
+ }
100
+ //# sourceMappingURL=change-utils.js.map
@@ -1,4 +1,9 @@
1
1
  export declare class FileSystemUtils {
2
+ /**
3
+ * Converts a path to use forward slashes (POSIX style).
4
+ * Essential for cross-platform compatibility with glob libraries like fast-glob.
5
+ */
6
+ static toPosixPath(p: string): string;
2
7
  private static isWindowsBasePath;
3
8
  private static normalizeSegments;
4
9
  static joinPath(basePath: string, ...segments: string[]): string;
@@ -30,6 +30,13 @@ function findMarkerIndex(content, marker, fromIndex = 0) {
30
30
  return -1;
31
31
  }
32
32
  export class FileSystemUtils {
33
+ /**
34
+ * Converts a path to use forward slashes (POSIX style).
35
+ * Essential for cross-platform compatibility with glob libraries like fast-glob.
36
+ */
37
+ static toPosixPath(p) {
38
+ return p.replace(/\\/g, '/');
39
+ }
33
40
  static isWindowsBasePath(basePath) {
34
41
  return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
35
42
  }
@@ -1,2 +1,4 @@
1
- export {};
1
+ export { validateChangeName, createChange } from './change-utils.js';
2
+ export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
3
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,5 @@
1
- export {};
1
+ // Shared utilities
2
+ export { validateChangeName, createChange } from './change-utils.js';
3
+ // Change metadata utilities
4
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
2
5
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "0.17.2",
3
+ "version": "0.18.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -32,6 +32,7 @@
32
32
  "files": [
33
33
  "dist",
34
34
  "bin",
35
+ "schemas",
35
36
  "scripts/postinstall.js",
36
37
  "!dist/**/*.test.js",
37
38
  "!dist/**/__tests__",
@@ -54,7 +55,9 @@
54
55
  "@inquirer/prompts": "^7.8.0",
55
56
  "chalk": "^5.5.0",
56
57
  "commander": "^14.0.0",
58
+ "fast-glob": "^3.3.3",
57
59
  "ora": "^8.2.0",
60
+ "yaml": "^2.8.2",
58
61
  "zod": "^4.0.17"
59
62
  },
60
63
  "scripts": {
@@ -0,0 +1,148 @@
1
+ name: spec-driven
2
+ version: 1
3
+ description: Default OpenSpec workflow - proposal → specs → design → tasks
4
+ artifacts:
5
+ - id: proposal
6
+ generates: proposal.md
7
+ description: Initial proposal document outlining the change
8
+ template: proposal.md
9
+ instruction: |
10
+ Create the proposal document that establishes WHY this change is needed.
11
+
12
+ Sections:
13
+ - **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
14
+ - **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
15
+ - **Capabilities**: Identify which specs will be created or modified:
16
+ - **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<name>/spec.md`. Use kebab-case names (e.g., `user-auth`, `data-export`).
17
+ - **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Check `openspec/specs/` for existing spec names. Leave empty if no requirement changes.
18
+ - **Impact**: Affected code, APIs, dependencies, or systems.
19
+
20
+ IMPORTANT: The Capabilities section is critical. It creates the contract between
21
+ proposal and specs phases. Research existing specs before filling this in.
22
+ Each capability listed here will need a corresponding spec file.
23
+
24
+ Keep it concise (1-2 pages). Focus on the "why" not the "how" -
25
+ implementation details belong in design.md.
26
+
27
+ This is the foundation - specs, design, and tasks all build on this.
28
+ requires: []
29
+
30
+ - id: specs
31
+ generates: "specs/**/*.md"
32
+ description: Detailed specifications for the change
33
+ template: spec.md
34
+ instruction: |
35
+ Create specification files that define WHAT the system should do.
36
+
37
+ Create one spec file per capability/feature area in specs/<name>/spec.md.
38
+
39
+ Delta operations (use ## headers):
40
+ - **ADDED Requirements**: New capabilities
41
+ - **MODIFIED Requirements**: Changed behavior - MUST include full updated content
42
+ - **REMOVED Requirements**: Deprecated features - MUST include **Reason** and **Migration**
43
+ - **RENAMED Requirements**: Name changes only - use FROM:/TO: format
44
+
45
+ Format requirements:
46
+ - Each requirement: `### Requirement: <name>` followed by description
47
+ - Use SHALL/MUST for normative requirements (avoid should/may)
48
+ - Each scenario: `#### Scenario: <name>` with WHEN/THEN format
49
+ - **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
50
+ - Every requirement MUST have at least one scenario.
51
+
52
+ MODIFIED requirements workflow:
53
+ 1. Locate the existing requirement in openspec/specs/<capability>/spec.md
54
+ 2. Copy the ENTIRE requirement block (from `### Requirement:` through all scenarios)
55
+ 3. Paste under `## MODIFIED Requirements` and edit to reflect new behavior
56
+ 4. Ensure header text matches exactly (whitespace-insensitive)
57
+
58
+ Common pitfall: Using MODIFIED with partial content loses detail at archive time.
59
+ If adding new concerns without changing existing behavior, use ADDED instead.
60
+
61
+ Example:
62
+ ```
63
+ ## ADDED Requirements
64
+
65
+ ### Requirement: User can export data
66
+ The system SHALL allow users to export their data in CSV format.
67
+
68
+ #### Scenario: Successful export
69
+ - **WHEN** user clicks "Export" button
70
+ - **THEN** system downloads a CSV file with all user data
71
+
72
+ ## REMOVED Requirements
73
+
74
+ ### Requirement: Legacy export
75
+ **Reason**: Replaced by new export system
76
+ **Migration**: Use new export endpoint at /api/v2/export
77
+ ```
78
+
79
+ Specs should be testable - each scenario is a potential test case.
80
+ requires:
81
+ - proposal
82
+
83
+ - id: design
84
+ generates: design.md
85
+ description: Technical design document with implementation details
86
+ template: design.md
87
+ instruction: |
88
+ Create the design document that explains HOW to implement the change.
89
+
90
+ When to include design.md (create only if any apply):
91
+ - Cross-cutting change (multiple services/modules) or new architectural pattern
92
+ - New external dependency or significant data model changes
93
+ - Security, performance, or migration complexity
94
+ - Ambiguity that benefits from technical decisions before coding
95
+
96
+ Sections:
97
+ - **Context**: Background, current state, constraints, stakeholders
98
+ - **Goals / Non-Goals**: What this design achieves and explicitly excludes
99
+ - **Decisions**: Key technical choices with rationale (why X over Y?). Include alternatives considered for each decision.
100
+ - **Risks / Trade-offs**: Known limitations, things that could go wrong. Format: [Risk] → Mitigation
101
+ - **Migration Plan**: Steps to deploy, rollback strategy (if applicable)
102
+ - **Open Questions**: Outstanding decisions or unknowns to resolve
103
+
104
+ Focus on architecture and approach, not line-by-line implementation.
105
+ Reference the proposal for motivation and specs for requirements.
106
+
107
+ Good design docs explain the "why" behind technical decisions.
108
+ requires:
109
+ - proposal
110
+
111
+ - id: tasks
112
+ generates: tasks.md
113
+ description: Implementation tasks derived from specs and design
114
+ template: tasks.md
115
+ instruction: |
116
+ Create the task list that breaks down the implementation work.
117
+
118
+ Guidelines:
119
+ - Group related tasks under ## numbered headings
120
+ - Each task is a checkbox: - [ ] X.Y Task description
121
+ - Tasks should be small enough to complete in one session
122
+ - Order tasks by dependency (what must be done first?)
123
+
124
+ Example:
125
+ ```
126
+ ## 1. Setup
127
+
128
+ - [ ] 1.1 Create new module structure
129
+ - [ ] 1.2 Add dependencies to package.json
130
+
131
+ ## 2. Core Implementation
132
+
133
+ - [ ] 2.1 Implement data export function
134
+ - [ ] 2.2 Add CSV formatting utilities
135
+ ```
136
+
137
+ Reference specs for what needs to be built, design for how to build it.
138
+ Each task should be verifiable - you know when it's done.
139
+ requires:
140
+ - specs
141
+ - design
142
+
143
+ apply:
144
+ requires: [tasks]
145
+ tracks: tasks.md
146
+ instruction: |
147
+ Read context files, work through pending tasks, mark complete as you go.
148
+ Pause if you hit blockers or need clarification.