mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fbda520de | ||
|
|
5633556b6d | ||
|
|
2bb0ed36c5 | ||
|
|
06097f9cb7 | ||
|
|
8f5a526396 | ||
|
|
eb152eb2ca | ||
|
|
e987a5a327 | ||
|
|
4971cda812 |
@@ -18,9 +18,20 @@ jobs:
|
||||
if: github.repository == 'Fission-AI/OpenSpec'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Generate GitHub App token first - used for checkout and changesets
|
||||
# This allows git operations to trigger CI workflows on the version PR
|
||||
# (GITHUB_TOKEN cannot trigger workflows by design)
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -44,5 +55,5 @@ jobs:
|
||||
# so package.json already contains the bumped version.
|
||||
publish: pnpm run release:ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
# npm authentication handled via OIDC trusted publishing (no token needed)
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- eb152eb: ### New Features
|
||||
|
||||
- **Continue IDE support** – OpenSpec now generates slash commands for [Continue](https://continue.dev/), expanding editor integration options alongside Cursor, Windsurf, Claude Code, and others
|
||||
- **Shell completions for Bash, Fish, and PowerShell** – Run `openspec completion install` to set up tab completion in your preferred shell
|
||||
- **`/opsx:explore` command** – A new thinking partner mode for exploring ideas and investigating problems before committing to changes
|
||||
- **Codebuddy slash command improvements** – Updated frontmatter format for better compatibility
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Shell completions now correctly offer parent-level flags (like `--help`) when a command has subcommands
|
||||
- Fixed Windows compatibility issues in tests
|
||||
|
||||
### Other
|
||||
|
||||
- Added optional anonymous usage statistics to help understand how OpenSpec is used. This is **opt-out** by default – set `OPENSPEC_TELEMETRY=0` or `DO_NOT_TRACK=1` to disable. Only command names and version are collected; no arguments, file paths, or content. Automatically disabled in CI environments.
|
||||
|
||||
## 0.18.0
|
||||
|
||||
### Minor Changes
|
||||
@@ -86,6 +106,8 @@
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add Continue slash command support so `openspec init` can generate `.continue/prompts/openspec-*.prompt` files with MARKDOWN frontmatter and `$ARGUMENTS` placeholder, and refresh them on `openspec update`.
|
||||
|
||||
- Add Antigravity slash command support so `openspec init` can generate `.agent/workflows/openspec-*.md` files with description-only frontmatter and `openspec update` refreshes existing workflows alongside Windsurf.
|
||||
|
||||
## 0.15.0
|
||||
|
||||
@@ -103,6 +103,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
|
||||
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
|
||||
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
|
||||
| **Continue** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.continue/prompts/`) |
|
||||
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
|
||||
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
|
||||
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
|
||||
|
||||
@@ -211,6 +211,12 @@ The init command SHALL generate slash command files for supported editors using
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Continue
|
||||
- **WHEN** the user selects Continue during initialization
|
||||
- **THEN** create `.continue/prompts/openspec-proposal.prompt`, `.continue/prompts/openspec-apply.prompt`, and `.continue/prompts/openspec-archive.prompt`
|
||||
- **AND** populate each file from shared templates so command text matches other tools
|
||||
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
|
||||
|
||||
#### Scenario: Generating slash commands for Factory Droid
|
||||
- **WHEN** the user selects Factory Droid during initialization
|
||||
- **THEN** create `.factory/commands/openspec-proposal.md`, `.factory/commands/openspec-apply.md`, and `.factory/commands/openspec-archive.md`
|
||||
|
||||
@@ -75,6 +75,11 @@ The update command SHALL refresh existing slash command files for configured too
|
||||
- **AND** include Cline-specific Markdown heading frontmatter
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Continue
|
||||
- **WHEN** `.continue/prompts/` contains `openspec-proposal.prompt`, `openspec-apply.prompt`, and `openspec-archive.prompt`
|
||||
- **THEN** refresh each file using shared templates
|
||||
- **AND** ensure templates include instructions for the relevant workflow stage
|
||||
|
||||
#### Scenario: Updating slash commands for Crush
|
||||
- **WHEN** `.crush/commands/` contains `openspec/proposal.md`, `openspec/apply.md`, and `openspec/archive.md`
|
||||
- **THEN** refresh each file using shared templates
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
+6
-3
@@ -50,7 +50,10 @@ program
|
||||
program.option('--no-color', 'Disable color output');
|
||||
|
||||
// Apply global flags and telemetry before any command runs
|
||||
program.hook('preAction', async (thisCommand) => {
|
||||
// Note: preAction receives (thisCommand, actionCommand) where:
|
||||
// - thisCommand: the command where hook was added (root program)
|
||||
// - actionCommand: the command actually being executed (subcommand)
|
||||
program.hook('preAction', async (thisCommand, actionCommand) => {
|
||||
const opts = thisCommand.opts();
|
||||
if (opts.color === false) {
|
||||
process.env.NO_COLOR = '1';
|
||||
@@ -59,8 +62,8 @@ program.hook('preAction', async (thisCommand) => {
|
||||
// Show first-run telemetry notice (if not seen)
|
||||
await maybeShowTelemetryNotice();
|
||||
|
||||
// Track command execution
|
||||
const commandPath = getCommandPath(thisCommand);
|
||||
// Track command execution (use actionCommand to get the actual subcommand)
|
||||
const commandPath = getCommandPath(actionCommand);
|
||||
await trackCommand(commandPath, version);
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
|
||||
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
|
||||
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
|
||||
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)' },
|
||||
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
|
||||
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
|
||||
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.continue/prompts/openspec-proposal.prompt',
|
||||
apply: '.continue/prompts/openspec-apply.prompt',
|
||||
archive: '.continue/prompts/openspec-archive.prompt'
|
||||
};
|
||||
|
||||
/*
|
||||
* Continue .prompt format requires YAML frontmatter:
|
||||
* ---
|
||||
* name: commandName
|
||||
* description: description
|
||||
* invokable: true
|
||||
* ---
|
||||
* Body...
|
||||
*
|
||||
* The 'invokable: true' field is required to make the prompt available as a slash command.
|
||||
* We use 'openspec-proposal' as the name so the command becomes /openspec-proposal.
|
||||
*/
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
name: openspec-proposal
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
invokable: true
|
||||
---`,
|
||||
apply: `---
|
||||
name: openspec-apply
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
invokable: true
|
||||
---`,
|
||||
archive: `---
|
||||
name: openspec-archive
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
invokable: true
|
||||
---`
|
||||
};
|
||||
|
||||
export class ContinueSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'continue';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { QwenSlashCommandConfigurator } from './qwen.js';
|
||||
import { RooCodeSlashCommandConfigurator } from './roocode.js';
|
||||
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
|
||||
import { IflowSlashCommandConfigurator } from './iflow.js';
|
||||
import { ContinueSlashCommandConfigurator } from './continue.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
|
||||
@@ -44,6 +45,7 @@ export class SlashCommandRegistry {
|
||||
const roocode = new RooCodeSlashCommandConfigurator();
|
||||
const antigravity = new AntigravitySlashCommandConfigurator();
|
||||
const iflow = new IflowSlashCommandConfigurator();
|
||||
const continueTool = new ContinueSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(codeBuddy.toolId, codeBuddy);
|
||||
@@ -65,6 +67,7 @@ export class SlashCommandRegistry {
|
||||
this.configurators.set(roocode.toolId, roocode);
|
||||
this.configurators.set(antigravity.toolId, antigravity);
|
||||
this.configurators.set(iflow.toolId, iflow);
|
||||
this.configurators.set(continueTool.toolId, continueTool);
|
||||
}
|
||||
|
||||
static register(configurator: SlashCommandConfigurator): void {
|
||||
|
||||
@@ -1151,6 +1151,61 @@ describe('InitCommand', () => {
|
||||
expect(codeBuddyChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create Continue slash command files with templates', async () => {
|
||||
queueSelections('continue', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const continueProposal = path.join(
|
||||
testDir,
|
||||
'.continue/prompts/openspec-proposal.prompt'
|
||||
);
|
||||
const continueApply = path.join(
|
||||
testDir,
|
||||
'.continue/prompts/openspec-apply.prompt'
|
||||
);
|
||||
const continueArchive = path.join(
|
||||
testDir,
|
||||
'.continue/prompts/openspec-archive.prompt'
|
||||
);
|
||||
|
||||
expect(await fileExists(continueProposal)).toBe(true);
|
||||
expect(await fileExists(continueApply)).toBe(true);
|
||||
expect(await fileExists(continueArchive)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(continueProposal, 'utf-8');
|
||||
expect(proposalContent).toContain('---');
|
||||
expect(proposalContent).toContain('name: openspec-proposal');
|
||||
expect(proposalContent).toContain('invokable: true');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
|
||||
const applyContent = await fs.readFile(continueApply, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('name: openspec-apply');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('invokable: true');
|
||||
expect(applyContent).toContain('Work through tasks sequentially');
|
||||
|
||||
const archiveContent = await fs.readFile(continueArchive, 'utf-8');
|
||||
expect(archiveContent).toContain('---');
|
||||
expect(archiveContent).toContain('name: openspec-archive');
|
||||
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
|
||||
expect(archiveContent).toContain('invokable: true');
|
||||
expect(archiveContent).toContain('openspec archive <id> --yes');
|
||||
});
|
||||
|
||||
it('should mark Continue as already configured during extend mode', async () => {
|
||||
queueSelections('continue', DONE, 'continue', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const continueChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'continue'
|
||||
);
|
||||
expect(continueChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create CODEBUDDY.md when CodeBuddy is selected', async () => {
|
||||
queueSelections('codebuddy', DONE);
|
||||
|
||||
|
||||
@@ -368,6 +368,80 @@ Old body
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should refresh existing Continue prompt files', async () => {
|
||||
const continuePath = path.join(
|
||||
testDir,
|
||||
'.continue/prompts/openspec-apply.prompt'
|
||||
);
|
||||
await fs.mkdir(path.dirname(continuePath), { recursive: true });
|
||||
const initialContent = `---
|
||||
name: openspec-apply
|
||||
description: Old description
|
||||
invokable: true
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(continuePath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updated = await fs.readFile(continuePath, 'utf-8');
|
||||
expect(updated).toContain('name: openspec-apply');
|
||||
expect(updated).toContain('invokable: true');
|
||||
expect(updated).toContain('Work through tasks sequentially');
|
||||
expect(updated).not.toContain('Old body');
|
||||
|
||||
const [logMessage] = consoleSpy.mock.calls[0];
|
||||
expect(logMessage).toContain(
|
||||
'Updated OpenSpec instructions (openspec/AGENTS.md'
|
||||
);
|
||||
expect(logMessage).toContain('AGENTS.md (created)');
|
||||
expect(logMessage).toContain(
|
||||
'Updated slash commands: .continue/prompts/openspec-apply.prompt'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create missing Continue prompt files on update', async () => {
|
||||
const continueApply = path.join(
|
||||
testDir,
|
||||
'.continue/prompts/openspec-apply.prompt'
|
||||
);
|
||||
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(continueApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
continueApply,
|
||||
`---
|
||||
name: openspec-apply
|
||||
description: Old description
|
||||
invokable: true
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const continueProposal = path.join(
|
||||
testDir,
|
||||
'.continue/prompts/openspec-proposal.prompt'
|
||||
);
|
||||
const continueArchive = path.join(
|
||||
testDir,
|
||||
'.continue/prompts/openspec-archive.prompt'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(continueProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(continueArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing OpenCode slash command files', async () => {
|
||||
const openCodePath = path.join(
|
||||
testDir,
|
||||
|
||||
Reference in New Issue
Block a user