Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`) |
Expand Down
11 changes: 11 additions & 0 deletions openspec/changes/add-antigravity-support/proposal.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions openspec/changes/add-antigravity-support/tasks.md
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
28 changes: 28 additions & 0 deletions src/core/configurators/slash/antigravity.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions src/core/configurators/slash/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
32 changes: 32 additions & 0 deletions test/core/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Comment on lines +466 to +496
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

LGTM! Comprehensive update test for Antigravity workflows.

The test properly validates:

  • Workflow body is refreshed with latest content
  • Description frontmatter is preserved (critical!)
  • Old content is removed
  • auto_execution_mode is not added (distinguishing from Windsurf)
  • Console output confirms the update

Verify that there's a test ensuring missing Antigravity workflow files are NOT created during update (following the pattern from other tools):


🏁 Script executed:

#!/bin/bash
# Search for a test that verifies missing Antigravity files aren't created on update
rg -n -A10 "should not create missing.*Antigravity" test/core/update.test.ts

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for all Antigravity-related tests to understand what's covered
rg -n "Antigravity" test/core/update.test.ts | head -20

Length of output: 71


🏁 Script executed:

#!/bin/bash
# Search for all "should not create missing" tests to see the pattern used
rg -n -B2 "should not create missing" test/core/update.test.ts

Length of output: 1270


🏁 Script executed:

#!/bin/bash
# Search for all Windsurf tests to compare patterns (mentioned in original comment)
rg -n "Windsurf" test/core/update.test.ts | head -20

Length of output: 370


🏁 Script executed:

#!/bin/bash
# Check if there are any other Antigravity tests after line 470
rg -n "Antigravity" test/core/update.test.ts

Length of output: 71


🏁 Script executed:

#!/bin/bash
# Also search for tests after line 500 to confirm coverage
rg -n "should not create missing" test/core/update.test.ts | tail -5

Length of output: 441


Add missing test for Antigravity file creation prevention.

The test at lines 470-500 is comprehensive and correct. However, verification confirms that a corresponding test is missing: should not create missing Antigravity workflows on update.

All other tools in the test file (Windsurf, Qwen, Codex, GitHub Copilot, Factory, Amazon Q Developer, Auggie, CodeBuddy, Crush, RooCode, CoStrict, Qoder) include both a refresh test and a "should not create missing" test. Antigravity should follow the same pattern.

🤖 Prompt for AI Agents
In test/core/update.test.ts around lines 470 to 500, add a new unit test named
"should not create missing Antigravity workflows on update" that mirrors the
pattern used for other tools: ensure the .agent/workflows/openspec-apply.md file
does not exist beforehand, run updateCommand.execute(testDir), assert the file
was not created (fs.exists or attempting to read should fail), and verify no
"Updated slash commands: .agent/workflows/openspec-apply.md" console log was
emitted; use the same setup/teardown and console spying conventions as the
existing Antigravity refresh test so behavior is consistent with other tools'
"should not create missing" tests.


it('should refresh existing Codex prompts', async () => {
const codexPath = path.join(
testDir,
Expand Down
Loading