@fission-ai/openspec 0.8.0 → 0.9.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 CHANGED
@@ -15,7 +15,7 @@
15
15
  <a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
16
16
  <a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
17
17
  <a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
18
- <a href="https://discord.gg/saTQQGQZ"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
18
+ <a href="https://discord.gg/YctCnvvshC"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
19
19
  </p>
20
20
 
21
21
  <p align="center">
@@ -23,7 +23,7 @@
23
23
  </p>
24
24
 
25
25
  <p align="center">
26
- Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/saTQQGQZ">OpenSpec Discord</a> for help and questions.
26
+ Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
27
27
  </p>
28
28
 
29
29
  # OpenSpec
@@ -40,6 +40,15 @@ Key outcomes:
40
40
  - Shared visibility into what's proposed, active, or archived.
41
41
  - Works with the AI tools you already use: custom slash commands where supported, context rules everywhere else.
42
42
 
43
+ ## How OpenSpec compares (at a glance)
44
+
45
+ - **Lightweight**: simple workflow, no API keys, minimal setup.
46
+ - **Brownfield-first**: works great beyond 0→1. OpenSpec separates the source of truth from proposals: `openspec/specs/` (current truth) and `openspec/changes/` (proposed updates). This keeps diffs explicit and manageable across features.
47
+ - **Change tracking**: proposals, tasks, and spec deltas live together; archiving merges the approved updates back into specs.
48
+ - **Compared to spec-kit & Kiro**: those shine for brand-new features (0→1). OpenSpec also excels when modifying existing behavior (1→n), especially when updates span multiple specs.
49
+
50
+ See the full comparison in [How OpenSpec Compares](#how-openspec-compares).
51
+
43
52
  ## How It Works
44
53
 
45
54
  ```
@@ -86,6 +95,8 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
86
95
  | **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
87
96
  | **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
88
97
  | **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
98
+ | **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
99
+ | **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
89
100
 
90
101
  Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
91
102
 
@@ -94,7 +105,7 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`.
94
105
 
95
106
  | Tools |
96
107
  |-------|
97
- | Codex • Amp • Jules • Gemini CLI • GitHub Copilot • Others |
108
+ | Amp • Jules • Gemini CLI • Others |
98
109
 
99
110
  ### Install & Initialize
100
111
 
@@ -198,7 +209,7 @@ Or run the command yourself in terminal:
198
209
  $ openspec archive add-profile-filters --yes # Archive the completed change without prompts
199
210
  ```
200
211
 
201
- **Note:** Tools with native slash commands (Claude Code, Cursor) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change".
212
+ **Note:** Tools with native slash commands (Claude Code, Cursor, Codex) can use the shortcuts shown. All other tools work with natural language requests to "create an OpenSpec proposal", "apply the OpenSpec change", or "archive the change".
202
213
 
203
214
  ## Command Reference
204
215
 
@@ -296,6 +307,9 @@ Deltas are "patches" that show how specs change:
296
307
 
297
308
  ## How OpenSpec Compares
298
309
 
310
+ ### vs. spec-kit
311
+ OpenSpec’s two-folder model (`openspec/specs/` for the current truth, `openspec/changes/` for proposed updates) keeps state and diffs separate. This scales when you modify existing features or touch multiple specs. spec-kit is strong for greenfield/0→1 but provides less structure for cross-spec updates and evolving features.
312
+
299
313
  ### vs. Kiro.dev
300
314
  OpenSpec groups every change for a feature in one folder (`openspec/changes/feature-name/`), making it easy to track related specs, tasks, and designs together. Kiro spreads updates across multiple spec folders, which can make feature tracking harder.
301
315
 
@@ -9,6 +9,8 @@ export const AI_TOOLS = [
9
9
  { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
10
10
  { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
11
11
  { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
12
- { name: 'AGENTS.md (works with Codex, Amp, VS Code, GitHub Copilot, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
12
+ { name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
13
+ { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
14
+ { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
13
15
  ];
14
16
  //# sourceMappingURL=config.js.map
@@ -12,6 +12,7 @@ export declare abstract class SlashCommandConfigurator {
12
12
  updateExisting(projectPath: string, _openspecDir: string): Promise<string[]>;
13
13
  protected abstract getRelativePath(id: SlashCommandId): string;
14
14
  protected abstract getFrontmatter(id: SlashCommandId): string | undefined;
15
- private updateBody;
15
+ resolveAbsolutePath(projectPath: string, id: SlashCommandId): string;
16
+ protected updateBody(filePath: string, body: string): Promise<void>;
16
17
  }
17
18
  //# sourceMappingURL=base.d.ts.map
@@ -45,6 +45,12 @@ export class SlashCommandConfigurator {
45
45
  }
46
46
  return updated;
47
47
  }
48
+ // Resolve absolute path for a given slash command target. Subclasses may override
49
+ // to redirect to tool-specific locations (e.g., global directories).
50
+ resolveAbsolutePath(projectPath, id) {
51
+ const rel = this.getRelativePath(id);
52
+ return path.join(projectPath, rel);
53
+ }
48
54
  async updateBody(filePath, body) {
49
55
  const content = await FileSystemUtils.readFile(filePath);
50
56
  const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
@@ -0,0 +1,14 @@
1
+ import { SlashCommandConfigurator } from "./base.js";
2
+ import { SlashCommandId } from "../../templates/index.js";
3
+ export declare class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
4
+ readonly toolId = "codex";
5
+ readonly isAvailable = true;
6
+ protected getRelativePath(id: SlashCommandId): string;
7
+ protected getFrontmatter(id: SlashCommandId): string | undefined;
8
+ private getGlobalPromptsDir;
9
+ generateAll(projectPath: string, _openspecDir: string): Promise<string[]>;
10
+ updateExisting(projectPath: string, _openspecDir: string): Promise<string[]>;
11
+ private updateFullFile;
12
+ resolveAbsolutePath(_projectPath: string, id: SlashCommandId): string;
13
+ }
14
+ //# sourceMappingURL=codex.d.ts.map
@@ -0,0 +1,108 @@
1
+ import path from "path";
2
+ import os from "os";
3
+ import { SlashCommandConfigurator } from "./base.js";
4
+ import { TemplateManager } from "../../templates/index.js";
5
+ import { FileSystemUtils } from "../../../utils/file-system.js";
6
+ import { OPENSPEC_MARKERS } from "../../config.js";
7
+ const FILE_PATHS = {
8
+ proposal: ".codex/prompts/openspec-proposal.md",
9
+ apply: ".codex/prompts/openspec-apply.md",
10
+ archive: ".codex/prompts/openspec-archive.md",
11
+ };
12
+ export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
13
+ toolId = "codex";
14
+ isAvailable = true;
15
+ getRelativePath(id) {
16
+ return FILE_PATHS[id];
17
+ }
18
+ getFrontmatter(id) {
19
+ // Codex supports YAML frontmatter with description and argument-hint fields,
20
+ // plus $ARGUMENTS to capture all arguments as a single string.
21
+ const frontmatter = {
22
+ proposal: `---
23
+ description: Scaffold a new OpenSpec change and validate strictly.
24
+ argument-hint: request or feature description
25
+ ---
26
+
27
+ $ARGUMENTS`,
28
+ apply: `---
29
+ description: Implement an approved OpenSpec change and keep tasks in sync.
30
+ argument-hint: change-id
31
+ ---
32
+
33
+ $ARGUMENTS`,
34
+ archive: `---
35
+ description: Archive a deployed OpenSpec change and update specs.
36
+ argument-hint: change-id
37
+ ---
38
+
39
+ $ARGUMENTS`,
40
+ };
41
+ return frontmatter[id];
42
+ }
43
+ getGlobalPromptsDir() {
44
+ const home = (process.env.CODEX_HOME && process.env.CODEX_HOME.trim())
45
+ ? process.env.CODEX_HOME.trim()
46
+ : path.join(os.homedir(), ".codex");
47
+ return path.join(home, "prompts");
48
+ }
49
+ // Codex discovers prompts globally. Generate directly in the global directory
50
+ // and wrap shared body with markers.
51
+ async generateAll(projectPath, _openspecDir) {
52
+ const createdOrUpdated = [];
53
+ for (const target of this.getTargets()) {
54
+ const body = TemplateManager.getSlashCommandBody(target.id).trim();
55
+ const promptsDir = this.getGlobalPromptsDir();
56
+ const filePath = path.join(promptsDir, path.basename(target.path));
57
+ await FileSystemUtils.createDirectory(path.dirname(filePath));
58
+ if (await FileSystemUtils.fileExists(filePath)) {
59
+ await this.updateFullFile(filePath, target.id, body);
60
+ }
61
+ else {
62
+ const frontmatter = this.getFrontmatter(target.id);
63
+ const sections = [];
64
+ if (frontmatter)
65
+ sections.push(frontmatter.trim());
66
+ sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
67
+ await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n");
68
+ }
69
+ createdOrUpdated.push(target.path);
70
+ }
71
+ return createdOrUpdated;
72
+ }
73
+ async updateExisting(projectPath, _openspecDir) {
74
+ const updated = [];
75
+ for (const target of this.getTargets()) {
76
+ const promptsDir = this.getGlobalPromptsDir();
77
+ const filePath = path.join(promptsDir, path.basename(target.path));
78
+ if (await FileSystemUtils.fileExists(filePath)) {
79
+ const body = TemplateManager.getSlashCommandBody(target.id).trim();
80
+ await this.updateFullFile(filePath, target.id, body);
81
+ updated.push(target.path);
82
+ }
83
+ }
84
+ return updated;
85
+ }
86
+ // Update both frontmatter and body in an existing file
87
+ async updateFullFile(filePath, id, body) {
88
+ const content = await FileSystemUtils.readFile(filePath);
89
+ const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
90
+ if (startIndex === -1) {
91
+ throw new Error(`Missing OpenSpec start marker in ${filePath}`);
92
+ }
93
+ // Replace everything before the start marker with the new frontmatter
94
+ const frontmatter = this.getFrontmatter(id);
95
+ const sections = [];
96
+ if (frontmatter)
97
+ sections.push(frontmatter.trim());
98
+ sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
99
+ await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n");
100
+ }
101
+ // Resolve to the global prompts location for configuration detection
102
+ resolveAbsolutePath(_projectPath, id) {
103
+ const promptsDir = this.getGlobalPromptsDir();
104
+ const fileName = path.basename(FILE_PATHS[id]);
105
+ return path.join(promptsDir, fileName);
106
+ }
107
+ }
108
+ //# sourceMappingURL=codex.js.map
@@ -0,0 +1,9 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ import { SlashCommandId } from '../../templates/index.js';
3
+ export declare class GitHubCopilotSlashCommandConfigurator extends SlashCommandConfigurator {
4
+ readonly toolId = "github-copilot";
5
+ readonly isAvailable = true;
6
+ protected getRelativePath(id: SlashCommandId): string;
7
+ protected getFrontmatter(id: SlashCommandId): string;
8
+ }
9
+ //# sourceMappingURL=github-copilot.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { SlashCommandConfigurator } from './base.js';
2
+ const FILE_PATHS = {
3
+ proposal: '.github/prompts/openspec-proposal.prompt.md',
4
+ apply: '.github/prompts/openspec-apply.prompt.md',
5
+ archive: '.github/prompts/openspec-archive.prompt.md'
6
+ };
7
+ const FRONTMATTER = {
8
+ proposal: `---
9
+ description: Scaffold a new OpenSpec change and validate strictly.
10
+ ---
11
+
12
+ $ARGUMENTS`,
13
+ apply: `---
14
+ description: Implement an approved OpenSpec change and keep tasks in sync.
15
+ ---
16
+
17
+ $ARGUMENTS`,
18
+ archive: `---
19
+ description: Archive a deployed OpenSpec change and update specs.
20
+ ---
21
+
22
+ $ARGUMENTS`
23
+ };
24
+ export class GitHubCopilotSlashCommandConfigurator extends SlashCommandConfigurator {
25
+ toolId = 'github-copilot';
26
+ isAvailable = true;
27
+ getRelativePath(id) {
28
+ return FILE_PATHS[id];
29
+ }
30
+ getFrontmatter(id) {
31
+ return FRONTMATTER[id];
32
+ }
33
+ }
34
+ //# sourceMappingURL=github-copilot.js.map
@@ -3,6 +3,8 @@ import { CursorSlashCommandConfigurator } from './cursor.js';
3
3
  import { WindsurfSlashCommandConfigurator } from './windsurf.js';
4
4
  import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
5
5
  import { OpenCodeSlashCommandConfigurator } from './opencode.js';
6
+ import { CodexSlashCommandConfigurator } from './codex.js';
7
+ import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
6
8
  export class SlashCommandRegistry {
7
9
  static configurators = new Map();
8
10
  static {
@@ -11,11 +13,15 @@ export class SlashCommandRegistry {
11
13
  const windsurf = new WindsurfSlashCommandConfigurator();
12
14
  const kilocode = new KiloCodeSlashCommandConfigurator();
13
15
  const opencode = new OpenCodeSlashCommandConfigurator();
16
+ const codex = new CodexSlashCommandConfigurator();
17
+ const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
14
18
  this.configurators.set(claude.toolId, claude);
15
19
  this.configurators.set(cursor.toolId, cursor);
16
20
  this.configurators.set(windsurf.toolId, windsurf);
17
21
  this.configurators.set(kilocode.toolId, kilocode);
18
22
  this.configurators.set(opencode.toolId, opencode);
23
+ this.configurators.set(codex.toolId, codex);
24
+ this.configurators.set(githubCopilot.toolId, githubCopilot);
19
25
  }
20
26
  static register(configurator) {
21
27
  this.configurators.set(configurator.toolId, configurator);
package/dist/core/init.js CHANGED
@@ -331,7 +331,7 @@ export class InitCommand {
331
331
  kind: 'heading',
332
332
  value: OTHER_TOOLS_HEADING_VALUE,
333
333
  label: {
334
- primary: 'Other tools (use Universal AGENTS.md for Codex, Amp, VS Code, GitHub Copilot, …)',
334
+ primary: 'Other tools (use Universal AGENTS.md for Amp, VS Code, GitHub Copilot, …)',
335
335
  },
336
336
  selectable: false,
337
337
  },
@@ -369,7 +369,8 @@ export class InitCommand {
369
369
  if (!slashConfigurator)
370
370
  return false;
371
371
  for (const target of slashConfigurator.getTargets()) {
372
- if (await FileSystemUtils.fileExists(path.join(projectPath, target.path)))
372
+ const absolute = slashConfigurator.resolveAbsolutePath(projectPath, target.id);
373
+ if (await FileSystemUtils.fileExists(absolute))
373
374
  return true;
374
375
  }
375
376
  return false;
@@ -470,6 +471,13 @@ export class InitCommand {
470
471
  console.log(PALETTE.lightGray(' "Please explain the OpenSpec workflow from openspec/AGENTS.md'));
471
472
  console.log(PALETTE.lightGray(' and how I should work with you on this project"'));
472
473
  console.log(PALETTE.darkGray('────────────────────────────────────────────────────────────\n'));
474
+ // Codex heads-up: prompts installed globally
475
+ const selectedToolIds = new Set(selectedTools.map((t) => t.value));
476
+ if (selectedToolIds.has('codex')) {
477
+ console.log(PALETTE.white('Codex setup note'));
478
+ console.log(PALETTE.midGray('Prompts installed to ~/.codex/prompts (or $CODEX_HOME/prompts).'));
479
+ console.log();
480
+ }
473
481
  }
474
482
  formatToolNames(tools) {
475
483
  const names = tools
@@ -80,6 +80,7 @@ export class UpdateCommand {
80
80
  summaryParts.push(`Failed to update: ${failedItems.join(', ')}`);
81
81
  }
82
82
  console.log(summaryParts.join(' | '));
83
+ // No additional notes
83
84
  }
84
85
  }
85
86
  //# sourceMappingURL=update.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fission-ai/openspec",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -62,7 +62,10 @@
62
62
  "test:watch": "vitest",
63
63
  "test:ui": "vitest --ui",
64
64
  "test:coverage": "vitest --coverage",
65
- "release": "pnpm run build && pnpm exec changeset publish",
65
+ "check:pack-version": "node scripts/pack-version-check.mjs",
66
+ "release": "pnpm run release:ci",
67
+ "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
68
+ "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
66
69
  "changeset": "changeset"
67
70
  }
68
71
  }