diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b06a5b..2e731f29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 97cd3498..2590c7d0 100644 --- a/README.md +++ b/README.md @@ -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/`) | diff --git a/openspec/changes/add-antigravity-support/proposal.md b/openspec/changes/add-antigravity-support/proposal.md new file mode 100644 index 00000000..6ca49b45 --- /dev/null +++ b/openspec/changes/add-antigravity-support/proposal.md @@ -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 diff --git a/openspec/changes/add-antigravity-support/specs/cli-init/spec.md b/openspec/changes/add-antigravity-support/specs/cli-init/spec.md new file mode 100644 index 00000000..5b61bafc --- /dev/null +++ b/openspec/changes/add-antigravity-support/specs/cli-init/spec.md @@ -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: ` 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 diff --git a/openspec/changes/add-antigravity-support/specs/cli-update/spec.md b/openspec/changes/add-antigravity-support/specs/cli-update/spec.md new file mode 100644 index 00000000..8a2eebe4 --- /dev/null +++ b/openspec/changes/add-antigravity-support/specs/cli-update/spec.md @@ -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 diff --git a/openspec/changes/add-antigravity-support/tasks.md b/openspec/changes/add-antigravity-support/tasks.md new file mode 100644 index 00000000..e58d7b02 --- /dev/null +++ b/openspec/changes/add-antigravity-support/tasks.md @@ -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. diff --git a/src/core/config.ts b/src/core/config.ts index 3f1507c5..d0bc2288 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -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' }, diff --git a/src/core/configurators/slash/antigravity.ts b/src/core/configurators/slash/antigravity.ts new file mode 100644 index 00000000..f291f2a0 --- /dev/null +++ b/src/core/configurators/slash/antigravity.ts @@ -0,0 +1,28 @@ +import { SlashCommandConfigurator } from './base.js'; +import { SlashCommandId } from '../../templates/index.js'; + +const FILE_PATHS: Record = { + proposal: '.agent/workflows/openspec-proposal.md', + apply: '.agent/workflows/openspec-apply.md', + archive: '.agent/workflows/openspec-archive.md' +}; + +const DESCRIPTIONS: Record = { + 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---`; + } +} diff --git a/src/core/configurators/slash/registry.ts b/src/core/configurators/slash/registry.ts index afa7a691..45f9dae5 100644 --- a/src/core/configurators/slash/registry.ts +++ b/src/core/configurators/slash/registry.ts @@ -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 = 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 { diff --git a/test/core/init.test.ts b/test/core/init.test.ts index ca8469e7..1ebe4471 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -213,6 +213,50 @@ describe('InitCommand', () => { expect(archiveContent).toContain('Run `openspec archive --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(''); + 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(''); + 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(''); + expect(archiveContent).toContain('Run `openspec archive --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); diff --git a/test/core/update.test.ts b/test/core/update.test.ts index 05fa52f2..b53cca9c 100644 --- a/test/core/update.test.ts +++ b/test/core/update.test.ts @@ -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. +--- + + +Old body +`; + 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,