Compare commits

...
11 changed files with 167 additions and 0 deletions
+6
View File
@@ -1,5 +1,11 @@
# @fission-ai/openspec
## Unreleased
### Minor Changes
- 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
### Minor Changes
+1
View File
@@ -102,6 +102,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **OpenCode** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Kilo Code** | `/openspec-proposal.md`, `/openspec-apply.md`, `/openspec-archive.md` (`.kilocode/workflows/`) |
| **Qoder (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.qoder/commands/openspec/`) — see [docs](https://qoder.com/cli) |
| **Antigravity** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.agent/workflows/`) |
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
@@ -0,0 +1,11 @@
## Why
Google is rolling out Antigravity, a Windsurf-derived IDE that discovers workflows from `.agent/workflows/*.md`. Today OpenSpec can only scaffold slash commands for Windsurf directories, so Antigravity users cannot run the proposal/apply/archive flows from the IDE.
## What Changes
- Add Antigravity as a selectable native tool in `openspec init` so it creates `.agent/workflows/openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` with YAML frontmatter containing only a `description` field plus the standard OpenSpec-managed body.
- Ensure `openspec update` refreshes the body of any existing Antigravity workflows inside `.agent/workflows/` without creating missing files, mirroring the Windsurf behavior.
- Share e2e/template coverage confirming the generator writes the proper directory, filename casing, and frontmatter format so Antigravity picks up the workflows.
## Impact
- Affected specs: `specs/cli-init`, `specs/cli-update`
- Expected code: CLI init/update tool registries, slash-command templates, associated tests
@@ -0,0 +1,9 @@
## MODIFIED Requirements
### Requirement: Slash Command Configuration
The init command SHALL generate slash command files for supported editors using shared templates.
#### Scenario: Generating slash commands for Antigravity
- **WHEN** the user selects Antigravity during initialization
- **THEN** create `.agent/workflows/openspec-proposal.md`, `.agent/workflows/openspec-apply.md`, and `.agent/workflows/openspec-archive.md`
- **AND** ensure each file begins with YAML frontmatter that contains only a `description: <stage summary>` field followed by the shared OpenSpec workflow instructions wrapped in managed markers
- **AND** populate the workflow body with the same proposal/apply/archive guidance used for other tools so Antigravity behaves like Windsurf while pointing to the `.agent/workflows/` directory
@@ -0,0 +1,8 @@
## MODIFIED Requirements
### Requirement: Slash Command Updates
The update command SHALL refresh existing slash command files for configured tools without creating new ones, and ensure the OpenCode archive command accepts change ID arguments.
#### Scenario: Updating slash commands for Antigravity
- **WHEN** `.agent/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh the OpenSpec-managed portion of each file so the workflow copy matches other tools while preserving the existing single-field `description` frontmatter
- **AND** skip creating any missing workflow files during update, mirroring the behavior for Windsurf and other IDEs
@@ -0,0 +1,12 @@
## 1. CLI init support
- [x] 1.1 Surface Antigravity in the native-tool picker (interactive + `--tools`) so it toggles alongside other IDEs.
- [x] 1.2 Generate `.agent/workflows/openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md` with YAML frontmatter restricted to a single `description` field for each stage and wrap the body in OpenSpec markers.
- [x] 1.3 Confirm workspace scaffolding covers missing directory creation and re-run scenarios so repeated init refreshes the managed block.
## 2. CLI update support
- [x] 2.1 Detect existing Antigravity workflow files during `openspec update` and refresh only the managed body, skipping creation when files are missing.
- [x] 2.2 Ensure update logic preserves the `description` frontmatter block exactly as written by init, including case and spacing, and refreshes body templates alongside other tools.
## 3. Templates and tests
- [x] 3.1 Add shared template entries for Antigravity that reuse the Windsurf copy but target `.agent/workflows` plus the description-only frontmatter requirement.
- [x] 3.2 Expand automated coverage (unit or integration) verifying init and update produce the expected file paths and frontmatter + body markers for Antigravity.
+1
View File
@@ -30,6 +30,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity' },
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
@@ -0,0 +1,28 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.agent/workflows/openspec-proposal.md',
apply: '.agent/workflows/openspec-apply.md',
archive: '.agent/workflows/openspec-archive.md'
};
const DESCRIPTIONS: Record<SlashCommandId, string> = {
proposal: 'Scaffold a new OpenSpec change and validate strictly.',
apply: 'Implement an approved OpenSpec change and keep tasks in sync.',
archive: 'Archive a deployed OpenSpec change and update specs.'
};
export class AntigravitySlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'antigravity';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(id: SlashCommandId): string | undefined {
const description = DESCRIPTIONS[id];
return `---\ndescription: ${description}\n---`;
}
}
+3
View File
@@ -17,6 +17,7 @@ import { CrushSlashCommandConfigurator } from './crush.js';
import { CostrictSlashCommandConfigurator } from './costrict.js';
import { QwenSlashCommandConfigurator } from './qwen.js';
import { RooCodeSlashCommandConfigurator } from './roocode.js';
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -40,6 +41,7 @@ export class SlashCommandRegistry {
const costrict = new CostrictSlashCommandConfigurator();
const qwen = new QwenSlashCommandConfigurator();
const roocode = new RooCodeSlashCommandConfigurator();
const antigravity = new AntigravitySlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -59,6 +61,7 @@ export class SlashCommandRegistry {
this.configurators.set(costrict.toolId, costrict);
this.configurators.set(qwen.toolId, qwen);
this.configurators.set(roocode.toolId, roocode);
this.configurators.set(antigravity.toolId, antigravity);
}
static register(configurator: SlashCommandConfigurator): void {
+56
View File
@@ -213,6 +213,50 @@ describe('InitCommand', () => {
expect(archiveContent).toContain('Run `openspec archive <id> --yes`');
});
it('should create Antigravity workflows when Antigravity is selected', async () => {
queueSelections('antigravity', DONE);
await initCommand.execute(testDir);
const agProposal = path.join(
testDir,
'.agent/workflows/openspec-proposal.md'
);
const agApply = path.join(
testDir,
'.agent/workflows/openspec-apply.md'
);
const agArchive = path.join(
testDir,
'.agent/workflows/openspec-archive.md'
);
expect(await fileExists(agProposal)).toBe(true);
expect(await fileExists(agApply)).toBe(true);
expect(await fileExists(agArchive)).toBe(true);
const proposalContent = await fs.readFile(agProposal, 'utf-8');
expect(proposalContent).toContain('---');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
expect(proposalContent).not.toContain('auto_execution_mode');
const applyContent = await fs.readFile(agApply, 'utf-8');
expect(applyContent).toContain('---');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
expect(applyContent).toContain('Work through tasks sequentially');
expect(applyContent).not.toContain('auto_execution_mode');
const archiveContent = await fs.readFile(agArchive, 'utf-8');
expect(archiveContent).toContain('---');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('<!-- OPENSPEC:START -->');
expect(archiveContent).toContain('Run `openspec archive <id> --yes`');
expect(archiveContent).not.toContain('auto_execution_mode');
});
it('should always create AGENTS.md in project root', async () => {
queueSelections(DONE);
@@ -849,6 +893,18 @@ describe('InitCommand', () => {
expect(wsChoice.configured).toBe(true);
});
it('should mark Antigravity as already configured during extend mode', async () => {
queueSelections('antigravity', DONE, 'antigravity', DONE);
await initCommand.execute(testDir);
await initCommand.execute(testDir);
const secondRunArgs = mockPrompt.mock.calls[1][0];
const antigravityChoice = secondRunArgs.choices.find(
(choice: any) => choice.value === 'antigravity'
);
expect(antigravityChoice.configured).toBe(true);
});
it('should mark Codex as already configured during extend mode', async () => {
queueSelections('codex', DONE, 'codex', DONE);
await initCommand.execute(testDir);
+32
View File
@@ -463,6 +463,38 @@ Old body
consoleSpy.mockRestore();
});
it('should refresh existing Antigravity workflows', async () => {
const agPath = path.join(
testDir,
'.agent/workflows/openspec-apply.md'
);
await fs.mkdir(path.dirname(agPath), { recursive: true });
const initialContent = `---
description: Implement an approved OpenSpec change and keep tasks in sync.
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(agPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(agPath, 'utf-8');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(updated).not.toContain('auto_execution_mode: 3');
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
'Updated slash commands: .agent/workflows/openspec-apply.md'
);
consoleSpy.mockRestore();
});
it('should refresh existing Codex prompts', async () => {
const codexPath = path.join(
testDir,