@fission-ai/openspec 1.13.2 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/cli/commands/config.d.ts +8 -0
  2. package/dist/cli/commands/config.js +88 -0
  3. package/dist/cli/commands/context.d.ts +3 -0
  4. package/dist/cli/commands/context.js +20 -0
  5. package/dist/cli/commands/doctor.d.ts +3 -0
  6. package/dist/cli/commands/doctor.js +18 -0
  7. package/dist/cli/commands/schema.d.ts +6 -0
  8. package/dist/cli/commands/schema.js +57 -0
  9. package/dist/cli/commands/spec.d.ts +3 -0
  10. package/dist/cli/commands/spec.js +42 -0
  11. package/dist/cli/commands/store.d.ts +3 -0
  12. package/dist/cli/commands/store.js +130 -0
  13. package/dist/cli/commands/workset.d.ts +3 -0
  14. package/dist/cli/commands/workset.js +96 -0
  15. package/dist/cli/index.js +106 -43
  16. package/dist/commands/config.d.ts +16 -7
  17. package/dist/commands/config.js +342 -395
  18. package/dist/commands/context.d.ts +8 -2
  19. package/dist/commands/context.js +22 -37
  20. package/dist/commands/doctor.d.ts +6 -7
  21. package/dist/commands/doctor.js +14 -27
  22. package/dist/commands/schema.d.ts +19 -5
  23. package/dist/commands/schema.js +662 -702
  24. package/dist/commands/spec.d.ts +13 -4
  25. package/dist/commands/spec.js +92 -120
  26. package/dist/commands/store.d.ts +27 -2
  27. package/dist/commands/store.js +1 -120
  28. package/dist/commands/workflow/default-schema.d.ts +2 -0
  29. package/dist/commands/workflow/default-schema.js +4 -0
  30. package/dist/commands/workflow/instructions.js +32 -12
  31. package/dist/commands/workflow/shared.d.ts +3 -1
  32. package/dist/commands/workflow/shared.js +1 -1
  33. package/dist/commands/workflow/status.js +15 -2
  34. package/dist/commands/workset.d.ts +22 -2
  35. package/dist/commands/workset.js +1 -88
  36. package/dist/core/archive.js +28 -7
  37. package/dist/core/artifact-graph/instruction-loader.d.ts +7 -0
  38. package/dist/core/artifact-graph/instruction-loader.js +18 -1
  39. package/dist/core/change-metadata/schema.d.ts +3 -0
  40. package/dist/core/change-metadata/schema.js +10 -0
  41. package/dist/core/change-status-policy.d.ts +8 -0
  42. package/dist/core/change-status-policy.js +33 -2
  43. package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
  44. package/dist/core/command-generation/adapters/atomcode.js +46 -0
  45. package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
  46. package/dist/core/command-generation/adapters/codestudio.js +27 -0
  47. package/dist/core/command-generation/adapters/easycode.d.ts +17 -0
  48. package/dist/core/command-generation/adapters/easycode.js +28 -0
  49. package/dist/core/command-generation/adapters/gemini.js +1 -35
  50. package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
  51. package/dist/core/command-generation/adapters/gigacode.js +31 -0
  52. package/dist/core/command-generation/adapters/index.d.ts +4 -0
  53. package/dist/core/command-generation/adapters/index.js +4 -0
  54. package/dist/core/command-generation/registry.js +8 -0
  55. package/dist/core/command-generation/toml.d.ts +19 -0
  56. package/dist/core/command-generation/toml.js +39 -0
  57. package/dist/core/command-surface.js +1 -1
  58. package/dist/core/completion-tip.js +3 -1
  59. package/dist/core/completions/command-registry.js +19 -0
  60. package/dist/core/completions/installers/zsh-installer.js +4 -2
  61. package/dist/core/config-prompts.js +6 -6
  62. package/dist/core/config-schema.d.ts +1 -1
  63. package/dist/core/config.js +11 -1
  64. package/dist/core/init.js +35 -11
  65. package/dist/core/legacy-cleanup.js +11 -7
  66. package/dist/core/list.d.ts +2 -0
  67. package/dist/core/list.js +49 -23
  68. package/dist/core/parsers/markdown-parser.js +5 -0
  69. package/dist/core/parsers/requirement-blocks.d.ts +6 -0
  70. package/dist/core/parsers/requirement-blocks.js +9 -2
  71. package/dist/core/project-config.js +45 -1
  72. package/dist/core/references.js +3 -1
  73. package/dist/core/root-selection.d.ts +6 -0
  74. package/dist/core/root-selection.js +16 -0
  75. package/dist/core/schemas/base.schema.d.ts +3 -0
  76. package/dist/core/schemas/base.schema.js +6 -0
  77. package/dist/core/schemas/change.schema.d.ts +8 -0
  78. package/dist/core/schemas/spec.schema.d.ts +2 -0
  79. package/dist/core/templates/workflows/apply-change.js +9 -5
  80. package/dist/core/templates/workflows/archive-change.js +26 -4
  81. package/dist/core/templates/workflows/bulk-archive-change.js +6 -2
  82. package/dist/core/templates/workflows/continue-change.js +17 -0
  83. package/dist/core/templates/workflows/verify-change.js +12 -12
  84. package/dist/core/validation/constants.d.ts +1 -1
  85. package/dist/core/validation/constants.js +1 -1
  86. package/dist/core/validation/validator.js +21 -2
  87. package/dist/core/version-check.d.ts +23 -1
  88. package/dist/core/version-check.js +112 -26
  89. package/dist/core/view.d.ts +2 -0
  90. package/dist/core/view.js +36 -2
  91. package/dist/utils/change-metadata.d.ts +20 -1
  92. package/dist/utils/change-metadata.js +57 -1
  93. package/dist/utils/command-references.d.ts +1 -1
  94. package/dist/utils/command-references.js +2 -2
  95. package/dist/utils/index.d.ts +1 -1
  96. package/dist/utils/index.js +1 -1
  97. package/package.json +1 -1
  98. package/schemas/spec-driven/schema.yaml +29 -4
  99. package/schemas/spec-driven/templates/proposal.md +6 -3
@@ -6,6 +6,7 @@ import { createRequire } from 'module';
6
6
  import chalk from 'chalk';
7
7
  import { isCiEnvironment } from '../utils/ci.js';
8
8
  import { isTelemetryOptedOutByEnv } from '../telemetry/opt-out.js';
9
+ import { FileSystemUtils } from '../utils/file-system.js';
9
10
  import { getGlobalConfig, isGlobalConfigUnreadable } from './global-config.js';
10
11
  const require = createRequire(import.meta.url);
11
12
  const { name: PACKAGE_NAME, version: OPENSPEC_VERSION } = require('../../package.json');
@@ -268,22 +269,34 @@ function fetchLatestVersion() {
268
269
  });
269
270
  }
270
271
  /**
271
- * Returns the published version when the installed CLI is behind it, otherwise
272
- * null. Never throws and never blocks for longer than the request timeout.
272
+ * Checks the registry while preserving enough detail for callers to explain
273
+ * why no newer version was reported. Never throws.
273
274
  */
274
- export async function getAvailableCliUpdate() {
275
- if (!isCheckEnabled())
276
- return null;
275
+ export async function checkForCliUpdate() {
276
+ if (!isCheckEnabled() || registryUrl() === null) {
277
+ return { status: 'disabled', latest: null };
278
+ }
277
279
  try {
278
280
  const latest = await fetchLatestVersion();
279
281
  if (!latest)
280
- return null;
281
- return compareVersions(latest, OPENSPEC_VERSION) > 0 ? latest : null;
282
+ return { status: 'offline', latest: null };
283
+ return {
284
+ status: compareVersions(latest, OPENSPEC_VERSION) > 0 ? 'available' : 'current',
285
+ latest,
286
+ };
282
287
  }
283
288
  catch {
284
- return null;
289
+ return { status: 'offline', latest: null };
285
290
  }
286
291
  }
292
+ /**
293
+ * Returns the published version when the installed CLI is behind it, otherwise
294
+ * null. Kept as the compatibility surface used by `openspec update`.
295
+ */
296
+ export async function getAvailableCliUpdate() {
297
+ const result = await checkForCliUpdate();
298
+ return result.status === 'available' ? result.latest : null;
299
+ }
287
300
  /**
288
301
  * Directory the running CLI was loaded from, or null when it cannot be
289
302
  * resolved. Shown in the upgrade hint so anyone who upgraded but still runs an
@@ -312,8 +325,8 @@ export function isProjectLocalInstall(installDir, projectPath = '.') {
312
325
  // Windows paths differ in case and drive-letter casing between sources.
313
326
  const normalize = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
314
327
  try {
315
- let dir = path.resolve(projectPath);
316
- const target = normalize(installDir);
328
+ let dir = FileSystemUtils.canonicalizeExistingPath(projectPath);
329
+ const target = normalize(FileSystemUtils.canonicalizeExistingPath(installDir));
317
330
  for (;;) {
318
331
  if (target.startsWith(normalize(path.join(dir, 'node_modules') + path.sep))) {
319
332
  return true;
@@ -443,32 +456,29 @@ export function isSourceCheckout(installDir) {
443
456
  return false;
444
457
  }
445
458
  }
446
- /**
447
- * The package manager that owns this copy, so the printed command is one the
448
- * user's setup will actually honor.
449
- */
450
- export function detectPackageManager(installDir) {
451
- // Lowercased because the Windows directories are capitalized and undotted:
452
- // %LOCALAPPDATA%\\Volta, \\Yarn\\Data, \\pnpm-cache.
459
+ function detectKnownPackageManager(installDir) {
453
460
  const segments = (installDir ?? '').split(/[\\/]/).map((segment) => segment.toLowerCase());
454
461
  const has = (...names) => names.some((name) => segments.includes(name));
455
- // The undotted spelling exists for Windows (%LOCALAPPDATA%\Volta), whose
456
- // layout nests tools\image; require both segments so a user or project
457
- // directory merely named "volta" (even one with its own "tools" dir) does
458
- // not steal the install.
459
462
  if (has('.volta') || (has('volta') && has('tools') && has('image')))
460
463
  return 'volta';
461
- if (has('.bun'))
464
+ if (has('.bun', '_bunx', 'bun-cache'))
462
465
  return 'bun';
463
- // These two need a corroborating segment: a directory merely named "pnpm" or
464
- // "yarn" (a user's home, a project) is not a global install of one.
465
- if (has('.pnpm-global', 'pnpm-cache'))
466
+ if (has('_npx'))
467
+ return 'npm';
468
+ if (has('.pnpm', '.pnpm-global', 'pnpm-cache'))
466
469
  return 'pnpm';
467
470
  if (has('pnpm') && has('global', 'dlx', 'store'))
468
471
  return 'pnpm';
469
472
  if (has('.yarn') || (has('yarn') && has('global')))
470
473
  return 'yarn';
471
- return 'npm';
474
+ return null;
475
+ }
476
+ /**
477
+ * The package manager that owns this copy, so the printed command is one the
478
+ * user's setup will actually honor.
479
+ */
480
+ export function detectPackageManager(installDir) {
481
+ return detectKnownPackageManager(installDir) ?? 'npm';
472
482
  }
473
483
  const GLOBAL_UPGRADE_COMMANDS = {
474
484
  npm: `npm install -g ${PACKAGE_NAME}@latest`,
@@ -477,6 +487,82 @@ const GLOBAL_UPGRADE_COMMANDS = {
477
487
  yarn: `yarn global add ${PACKAGE_NAME}@latest`,
478
488
  volta: `volta install ${PACKAGE_NAME}@latest`,
479
489
  };
490
+ function detectGlobalPackageManager(installDir) {
491
+ if (isNpmGlobalInstall(installDir))
492
+ return 'npm';
493
+ if (!installDir)
494
+ return null;
495
+ const segments = installDir.split(/[\\/]/).map((segment) => segment.toLowerCase());
496
+ const has = (...names) => names.some((name) => segments.includes(name));
497
+ const hasSequence = (...names) => segments.some((_, index) => names.every((name, offset) => segments[index + offset] === name));
498
+ if (hasSequence('.volta', 'tools', 'image') || hasSequence('volta', 'tools', 'image')) {
499
+ return 'volta';
500
+ }
501
+ if (has('.pnpm-global') || hasSequence('pnpm', 'global'))
502
+ return 'pnpm';
503
+ if (hasSequence('yarn', 'global') || hasSequence('yarn', 'data', 'global'))
504
+ return 'yarn';
505
+ if (hasSequence('.bun', 'install', 'global'))
506
+ return 'bun';
507
+ return null;
508
+ }
509
+ /** Describes the running copy without guessing when its owner is ambiguous. */
510
+ export function getCliInstallInfo(installDir = getInstallDir(), projectPath = '.') {
511
+ if (!installDir) {
512
+ return { location: null, packageManager: null, scope: null };
513
+ }
514
+ if (isSourceCheckout(installDir)) {
515
+ return { location: installDir, packageManager: null, scope: 'source' };
516
+ }
517
+ if (isEphemeralRunnerInstall(installDir)) {
518
+ return {
519
+ location: installDir,
520
+ packageManager: detectKnownPackageManager(installDir),
521
+ scope: 'temporary',
522
+ };
523
+ }
524
+ if (isProjectLocalInstall(installDir, projectPath)) {
525
+ return {
526
+ location: installDir,
527
+ packageManager: detectKnownPackageManager(installDir),
528
+ scope: 'project',
529
+ };
530
+ }
531
+ const packageManager = detectGlobalPackageManager(installDir);
532
+ return {
533
+ location: installDir,
534
+ packageManager,
535
+ scope: packageManager ? 'global' : null,
536
+ };
537
+ }
538
+ /** Returns a safe update command only for an install with known global ownership. */
539
+ export function getCliUpdateCommand(install) {
540
+ if (install.scope !== 'global' || install.packageManager === null)
541
+ return null;
542
+ return GLOBAL_UPGRADE_COMMANDS[install.packageManager];
543
+ }
544
+ /** Builds the concise human-readable form of the version report. */
545
+ export function buildVersionReportLines(version, install, update, command = null) {
546
+ const details = [install.packageManager, install.scope].filter(Boolean).join(', ');
547
+ const lines = [`OpenSpec ${version}${details ? ` (${details})` : ''}`];
548
+ if (!update)
549
+ return lines;
550
+ if (update.status === 'available') {
551
+ lines.push(`Update available: ${update.latest}`);
552
+ if (command)
553
+ lines.push(` ${command}`);
554
+ }
555
+ else if (update.status === 'current') {
556
+ lines.push('OpenSpec is up to date.');
557
+ }
558
+ else if (update.status === 'disabled') {
559
+ lines.push('Update check disabled.');
560
+ }
561
+ else {
562
+ lines.push('Could not check for updates.');
563
+ }
564
+ return lines;
565
+ }
480
566
  /**
481
567
  * Builds the hint, with the upgrade command chosen for how this copy of the CLI
482
568
  * was installed. Pure so every branch is assertable.
@@ -3,6 +3,8 @@ export declare class ViewCommand {
3
3
  private getChangesData;
4
4
  private getSpecsData;
5
5
  private displaySummary;
6
+ private sanitizeWorkflowText;
7
+ private formatWorkflowArtifacts;
6
8
  private createProgressBar;
7
9
  }
8
10
  //# sourceMappingURL=view.d.ts.map
package/dist/core/view.js CHANGED
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { getTaskProgressForChange } from '../utils/task-progress.js';
5
5
  import { MarkdownParser } from './parsers/markdown-parser.js';
6
6
  import { discoverSpecFiles } from '../utils/spec-discovery.js';
7
+ import { loadChangeContext, formatChangeStatus } from './artifact-graph/index.js';
7
8
  export class ViewCommand {
8
9
  async execute(targetPath = '.') {
9
10
  const openspecDir = path.join(targetPath, 'openspec');
@@ -30,12 +31,17 @@ export class ViewCommand {
30
31
  if (changesData.active.length > 0) {
31
32
  console.log(chalk.bold.cyan('\nActive Changes'));
32
33
  console.log('─'.repeat(60));
34
+ const maxNameLength = Math.min(48, Math.max(30, ...changesData.active.map((change) => change.name.length)));
33
35
  changesData.active.forEach((change) => {
34
36
  const progressBar = this.createProgressBar(change.progress.completed, change.progress.total);
35
37
  const percentage = change.progress.total > 0
36
38
  ? Math.round((change.progress.completed / change.progress.total) * 100)
37
39
  : 0;
38
- console.log(` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(30))} ${progressBar} ${chalk.dim(`${percentage}%`)}`);
40
+ console.log(` ${chalk.yellow('◉')} ${chalk.bold(change.name.padEnd(maxNameLength))} ${progressBar} ${chalk.dim(`${percentage}%`)}`);
41
+ if (change.workflowStatus) {
42
+ const { schemaName, artifacts } = change.workflowStatus;
43
+ console.log(` ${chalk.dim(`└─ [${this.sanitizeWorkflowText(schemaName)}]`)} ${this.formatWorkflowArtifacts(artifacts)}`);
44
+ }
39
45
  });
40
46
  }
41
47
  // Display completed changes
@@ -62,6 +68,7 @@ export class ViewCommand {
62
68
  }
63
69
  async getChangesData(openspecDir) {
64
70
  const changesDir = path.join(openspecDir, 'changes');
71
+ const projectRoot = path.dirname(openspecDir);
65
72
  if (!fs.existsSync(changesDir)) {
66
73
  return { draft: [], active: [], completed: [] };
67
74
  }
@@ -82,7 +89,15 @@ export class ViewCommand {
82
89
  }
83
90
  else {
84
91
  // Has tasks but not all complete
85
- active.push({ name: entry.name, progress });
92
+ let workflowStatus;
93
+ try {
94
+ workflowStatus = formatChangeStatus(loadChangeContext(projectRoot, entry.name));
95
+ }
96
+ catch (error) {
97
+ // Preserve task progress even when this change's workflow cannot be loaded.
98
+ console.warn(chalk.yellow(this.sanitizeWorkflowText(`Could not load workflow status for "${entry.name}": ${error instanceof Error ? error.message : String(error)}`)));
99
+ }
100
+ active.push({ name: entry.name, progress, workflowStatus });
86
101
  }
87
102
  }
88
103
  }
@@ -149,6 +164,25 @@ export class ViewCommand {
149
164
  console.log(` ${chalk.magenta('●')} Task Progress: ${chalk.bold(`${completedTasks}/${totalTasks}`)} (${overallProgress}% complete)`);
150
165
  }
151
166
  }
167
+ sanitizeWorkflowText(value) {
168
+ // Metadata may contain terminal controls; mask them before adding our own colors.
169
+ return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, '?');
170
+ }
171
+ formatWorkflowArtifacts(artifacts) {
172
+ return artifacts.map((artifact) => {
173
+ const id = this.sanitizeWorkflowText(artifact.id);
174
+ switch (artifact.status) {
175
+ case 'done':
176
+ return `${id}${chalk.green('✓')}`;
177
+ case 'ready':
178
+ return `${id}${chalk.cyan('→')}`;
179
+ case 'skipped':
180
+ return chalk.dim(`${id} (skipped)`);
181
+ case 'blocked':
182
+ return chalk.dim(id);
183
+ }
184
+ }).join(' ');
185
+ }
152
186
  createProgressBar(completed, total, width = 20) {
153
187
  if (total === 0)
154
188
  return chalk.dim('─'.repeat(width));
@@ -1,6 +1,25 @@
1
- import { type ChangeMetadata } from '../core/change-metadata/index.js';
1
+ import { CHANGE_METADATA_KNOWN_KEYS, type ChangeMetadata } from '../core/change-metadata/index.js';
2
2
  import { type ProjectConfig } from '../core/project-config.js';
3
3
  export declare const METADATA_FILENAME = ".openspec.yaml";
4
+ export { CHANGE_METADATA_KNOWN_KEYS };
5
+ /**
6
+ * Unknown top-level keys on a parsed .openspec.yaml object. Extra keys are
7
+ * stripped by ChangeMetadataSchema rather than rejected, so callers that want
8
+ * to tell the author a key did nothing have to look at the raw object.
9
+ */
10
+ export declare function listUnknownChangeMetadataKeys(parsed: unknown): string[];
11
+ /**
12
+ * Human-readable warning for keys ChangeMetadataSchema strips. Names the
13
+ * unknown keys, the keys that do exist, and (when present) why `skip_design`
14
+ * is not `skip_specs`.
15
+ */
16
+ export declare function formatUnknownChangeMetadataKeysMessage(keys: string[]): string;
17
+ /**
18
+ * Non-throwing read of unknown top-level keys. Missing, unreadable, or
19
+ * unparseable files yield no keys: those failures already have their own
20
+ * diagnostics on the read path.
21
+ */
22
+ export declare function readUnknownChangeMetadataKeys(changeDir: string): string[];
4
23
  /**
5
24
  * Error thrown when change metadata validation fails.
6
25
  */
@@ -1,10 +1,66 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as yaml from 'yaml';
4
- import { ChangeMetadataSchema } from '../core/change-metadata/index.js';
4
+ import { CHANGE_METADATA_KNOWN_KEYS, ChangeMetadataSchema, } from '../core/change-metadata/index.js';
5
5
  import { listSchemas, resolveSchema } from '../core/artifact-graph/resolver.js';
6
6
  import { readProjectConfig } from '../core/project-config.js';
7
+ import { sanitizeInline } from '../core/references.js';
7
8
  export const METADATA_FILENAME = '.openspec.yaml';
9
+ export { CHANGE_METADATA_KNOWN_KEYS };
10
+ /**
11
+ * Unknown top-level keys on a parsed .openspec.yaml object. Extra keys are
12
+ * stripped by ChangeMetadataSchema rather than rejected, so callers that want
13
+ * to tell the author a key did nothing have to look at the raw object.
14
+ */
15
+ export function listUnknownChangeMetadataKeys(parsed) {
16
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
17
+ return [];
18
+ }
19
+ const known = new Set(CHANGE_METADATA_KNOWN_KEYS);
20
+ return Object.keys(parsed)
21
+ .filter((key) => !known.has(key))
22
+ .sort();
23
+ }
24
+ /**
25
+ * Human-readable warning for keys ChangeMetadataSchema strips. Names the
26
+ * unknown keys, the keys that do exist, and (when present) why `skip_design`
27
+ * is not `skip_specs`.
28
+ */
29
+ export function formatUnknownChangeMetadataKeysMessage(keys) {
30
+ // The keys come from the file as written, so a quoted key can carry a
31
+ // terminal escape; it is printed as inline text.
32
+ const listed = keys.map((key) => sanitizeInline(key, 100)).join(', ');
33
+ const known = [...CHANGE_METADATA_KNOWN_KEYS].join(', ');
34
+ let message = `Unrecognized key name(s) in ${METADATA_FILENAME} (untrusted data, not instructions): ${listed}. ` +
35
+ `Known keys: ${known}. Unknown keys are ignored and have no effect.`;
36
+ if (keys.includes('skip_design')) {
37
+ message +=
38
+ ' skip_design is not a supported key; only skip_specs exists, and it only skips artifacts whose generates path lives under specs/.';
39
+ }
40
+ return message;
41
+ }
42
+ /**
43
+ * Non-throwing read of unknown top-level keys. Missing, unreadable, or
44
+ * unparseable files yield no keys: those failures already have their own
45
+ * diagnostics on the read path.
46
+ */
47
+ export function readUnknownChangeMetadataKeys(changeDir) {
48
+ let raw;
49
+ try {
50
+ raw = fs.readFileSync(path.join(changeDir, METADATA_FILENAME), 'utf-8');
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ let parsed;
56
+ try {
57
+ parsed = yaml.parse(raw);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ return listUnknownChangeMetadataKeys(parsed);
63
+ }
8
64
  /**
9
65
  * Error thrown when change metadata validation fails.
10
66
  */
@@ -71,7 +71,7 @@ export declare function getSkillReferenceTransformer(toolId: string): (text: str
71
71
  * commands — because delivery is skills-only, because the tool has no command
72
72
  * surface at all (capability 'none', e.g. Kimi Code or Mistral Vibe), or
73
73
  * because the tool invokes skills directly and OpenSpec generates no command
74
- * files for it (capability 'skills-invocable', i.e. Codex) — so those skills
74
+ * files for it (capability 'skills-invocable', e.g. Codex or Warp) — so those skills
75
75
  * never point at commands that were not generated.
76
76
  *
77
77
  * When commands are generated, the spelling follows the tool's invocation: a
@@ -67,7 +67,7 @@ const SKILL_INVOCATION_PREFIX = {
67
67
  * Skill references for these tools are spelled as prose ("the openspec-propose
68
68
  * skill") so skills-only delivery does not advertise unregistered commands.
69
69
  */
70
- const NATURAL_LANGUAGE_SKILL_TOOLS = new Set(['rovodev', 'codeassistant']);
70
+ const NATURAL_LANGUAGE_SKILL_TOOLS = new Set(['gsd', 'rovodev', 'codeassistant']);
71
71
  /**
72
72
  * Whether a tool references skills by natural language rather than a slash
73
73
  * command (see NATURAL_LANGUAGE_SKILL_TOOLS).
@@ -146,7 +146,7 @@ export function getSkillReferenceTransformer(toolId) {
146
146
  * commands — because delivery is skills-only, because the tool has no command
147
147
  * surface at all (capability 'none', e.g. Kimi Code or Mistral Vibe), or
148
148
  * because the tool invokes skills directly and OpenSpec generates no command
149
- * files for it (capability 'skills-invocable', i.e. Codex) — so those skills
149
+ * files for it (capability 'skills-invocable', e.g. Codex or Warp) — so those skills
150
150
  * never point at commands that were not generated.
151
151
  *
152
152
  * When commands are generated, the spelling follows the tool's invocation: a
@@ -1,6 +1,6 @@
1
1
  export { validateChangeName, createChange } from './change-utils.js';
2
2
  export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
3
- export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
3
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, listUnknownChangeMetadataKeys, readUnknownChangeMetadataKeys, formatUnknownChangeMetadataKeysMessage, CHANGE_METADATA_KNOWN_KEYS, } from './change-metadata.js';
4
4
  export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
5
5
  export { transformCommandInvocations, transformToSkillReferences, getSkillReferenceTransformer, getTransformerForTool, } from './command-references.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,7 @@
1
1
  // Shared utilities
2
2
  export { validateChangeName, createChange } from './change-utils.js';
3
3
  // Change metadata utilities
4
- export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
4
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, listUnknownChangeMetadataKeys, readUnknownChangeMetadataKeys, formatUnknownChangeMetadataKeysMessage, CHANGE_METADATA_KNOWN_KEYS, } from './change-metadata.js';
5
5
  // File system utilities
6
6
  export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
7
7
  // Command reference utilities
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "1.13.2",
3
+ "version": "1.14.1",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -13,7 +13,7 @@ artifacts:
13
13
  - **Why**: 1-2 sentences on the problem or opportunity. What problem does this solve? Why now?
14
14
  - **What Changes**: Bullet list of changes. Be specific about new capabilities, modifications, or removals. Mark breaking changes with **BREAKING**.
15
15
  - **Capabilities**: Identify which specs will be created or modified:
16
- - **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
16
+ - **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<capability-path>/spec.md`. Name each capability for a durable system behavior (for example, `user-auth`), not the work in this change (for example, `add-login-endpoint`). Choose a cohesive boundary that can own related requirements as the system evolves; avoid broad catch-all capabilities. Use kebab-case for path segments you introduce (e.g., `user-auth` or `identity/user-auth`) and follow the project's existing spec organization.
17
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. Use the exact existing path under `openspec/specs/`. Leave empty if no requirement changes.
18
18
  - **Impact**: Affected code, APIs, dependencies, or systems.
19
19
 
@@ -77,6 +77,17 @@ artifacts:
77
77
  - New capabilities: use the exact path from the proposal at `specs/<capability-path>/spec.md`. Any path segment newly introduced in the proposal must be kebab-case. Follow the project's existing organization; do not add a new domain level when the project uses a flat layout.
78
78
  - Modified capabilities: use the exact existing path from `openspec/specs/<capability-path>/` when creating the delta at `specs/<capability-path>/spec.md`. Run `openspec list --specs` to confirm that path before writing the delta, appending `--store "<id>"` only for a registered standalone store - a mistyped or invented path targets a capability that does not exist rather than the one you meant. Do not move or rename the capability.
79
79
 
80
+ Write the spec files one at a time, so the user sees progress and an
81
+ interrupted run keeps the files it already wrote:
82
+ - Before writing anything, compare the capabilities the proposal lists
83
+ with the `specs/<capability-path>/spec.md` files that already exist,
84
+ and start with the first capability that has no file. Do not rewrite
85
+ an existing file unless the user asks you to.
86
+ - Before each file, say in one line which capability you are writing.
87
+ - Write each file as soon as you have drafted it. Do not plan every file
88
+ before writing the first.
89
+ - Stop when every capability the proposal lists has its file.
90
+
80
91
  There must be at least one spec file unless the change's `.openspec.yaml`
81
92
  sets `skip_specs: true` (no spec-level behavior change) - `openspec validate`
82
93
  rejects a zero-delta change without that marker. If the proposal lists no
@@ -94,6 +105,7 @@ artifacts:
94
105
  - Each scenario: `#### Scenario: <name>` with WHEN/THEN format
95
106
  - **CRITICAL**: Scenarios MUST use exactly 4 hashtags (`####`). Using 3 hashtags or bullets will fail silently.
96
107
  - Every requirement MUST have at least one scenario.
108
+ - Keep each requirement's description (the text between `### Requirement:` and its first scenario) to 500 characters or fewer. `openspec validate` flags longer descriptions in ADDED requirements and in the main spec. This is a warning: normal validation still passes, but `openspec validate --strict` fails on it. When writing a new requirement, state one behavior per requirement: move examples and edge cases into scenarios, and split a requirement that covers several behaviors into separate `### Requirement:` blocks, each with its own scenarios. Under MODIFIED, keep the existing requirement block whole; never split, trim or rewrite existing text just to meet the length. Split an existing long requirement only when the user asks for it, in a change made for that purpose: under MODIFIED, keep its header and every scenario and cut its description down to one behavior without changing its meaning, then add each behavior you removed as its own ADDED requirement with its own scenarios.
97
109
 
98
110
  New capabilities only: the delta spec's first section is `## Purpose` -
99
111
  one or two sentences (50+ characters, or `openspec validate --strict`
@@ -194,17 +206,22 @@ artifacts:
194
206
  would change what gets built, resolve them with the user first - do not
195
207
  bake an unstated assumption into the task list.
196
208
 
197
- **IMPORTANT: Follow the template below exactly.** The apply phase parses
209
+ **IMPORTANT: Follow the template below for tracked tasks.** The apply phase parses
198
210
  checkbox format to track progress. A box holding only `x` counts as done,
199
211
  upper or lower case and with any spacing, so `- [ x]` is done too. Every
200
212
  other marker, including `- [~]`, `- [-]` and an empty `- []`, reads as
201
213
  unfinished. A line with no checkbox is not tracked at all.
202
214
 
203
215
  Guidelines:
204
- - Group related tasks under ## numbered headings
205
- - Each task MUST be a checkbox: `- [ ] X.Y Task description`
216
+ - Group related tracked tasks under ## numbered headings
217
+ - Each tracked task MUST be a checkbox: `- [ ] X.Y Task description`
206
218
  - Tasks should be small enough to complete in one session
207
219
  - Order tasks by dependency (what must be done first?)
220
+ - Track implementation and verification work that can be completed before
221
+ archive. If the requested workflow includes archive or work that requires
222
+ this change to be archived, preserve those steps as plain bullets in an
223
+ optional `## Workflow follow-up` section at the end of tasks.md. These
224
+ bullets are reference information outside tracked task progress.
208
225
  - Each task MUST state how to verify completion (a test, command,
209
226
  observable behavior, or delivered artifact). Put the verification in
210
227
  that task's checkbox description. Use a separate verification task only
@@ -234,6 +251,14 @@ artifacts:
234
251
  - [ ] 2.3 Document the export API in docs/export.md and verify the documented command runs as written
235
252
  ```
236
253
 
254
+ When applicable, append workflow follow-up as plain bullets, for example:
255
+ ```
256
+ ## Workflow follow-up
257
+
258
+ - Archive the change after the project's review requirements are satisfied.
259
+ - Verify the archived result.
260
+ ```
261
+
237
262
  Reference specs for what needs to be built, design for how to build it.
238
263
  requires:
239
264
  - specs
@@ -11,9 +11,12 @@
11
11
  ## Capabilities
12
12
 
13
13
  ### New Capabilities
14
- <!-- Capabilities being introduced. Use kebab-case for path segments you introduce
15
- (e.g., user-auth or identity/user-auth) that follow the project's existing
16
- spec organization. Each creates specs/<capability-path>/spec.md. -->
14
+ <!-- Capabilities being introduced. Name each capability for a cohesive system
15
+ behavior that can own related requirements as the system evolves. Do not name
16
+ implementation tasks or proposal sections. Avoid broad catch-all names. Use
17
+ kebab-case for path segments you introduce (e.g., user-auth or identity/user-auth)
18
+ that follow the project's existing spec organization. Each creates
19
+ specs/<capability-path>/spec.md. -->
17
20
  - `<capability-path>`: <brief description of what this capability covers>
18
21
 
19
22
  ### Modified Capabilities