Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale 91941243a3 docs: remove GitHub Copilot from tools list 2025-10-09 02:18:27 +11:00
Tabish Bidiwale 8822557bcd feat: add GitHub Copilot slash command support
Add GitHub Copilot as a natively supported AI tool with custom slash
commands for OpenSpec workflow operations. This enables teams using
GitHub Copilot to access /openspec-proposal, /openspec-apply, and
/openspec-archive directly from Copilot's chat interface.

Implementation:
- Create GitHubCopilotSlashCommandConfigurator that writes prompts to
  .github/prompts/ directory with YAML frontmatter and $ARGUMENTS
  placeholder following GitHub Copilot's prompt format
- Register GitHub Copilot in AI_TOOLS configuration and slash
  command registry for automatic init/update integration
- Add comprehensive test coverage for prompt generation, updates,
  and extend mode detection
- Update documentation (README and CHANGELOG) to include GitHub
  Copilot in the slash-command support table

The implementation follows the existing SlashCommandConfigurator
pattern and integrates seamlessly with openspec init and openspec
update commands.
2025-10-09 02:15:14 +11:00
5 changed files with 20 additions and 67 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@fission-ai/openspec": minor
---
Add support for Codex and GitHub Copilot slash commands with YAML frontmatter and $ARGUMENTS
@@ -1,13 +1,13 @@
## Why
- Codex (the VS Code extension formerly known as Codeium Chat) exposes "slash commands" by reading Markdown prompt files from `~/.codex/prompts/`. Each file name becomes the `/command` users can run, with YAML frontmatter for metadata (`description`, `argument-hint`) and `$ARGUMENTS` to capture user input. The workflow screenshot shared by Kevin Kern ("Codex problem analyzer") shows the format OpenSpec should target so teams can invoke curated workflows straight from the chat palette.
- Codex (the VS Code extension formerly known as Codeium Chat) exposes "slash commands" by reading Markdown prompt files from `~/.codex/prompts/`. Each file name becomes the `/command` users can run, with numbered placeholders (`$1`, `$2`, …) bound to the arguments they supply. The workflow screenshot shared by Kevin Kern ("Codex problem analyzer") shows the format OpenSpec should target so teams can invoke curated workflows straight from the chat palette.
- Teams already rely on OpenSpec to manage the slash-command surface area for Claude, Cursor, OpenCode, Kilo Code, and Windsurf. Leaving Codex out forces them to manually copy/paste OpenSpec guardrails into `~/.codex/prompts/*.md`, which drifts quickly and undermines the "single source of truth" promise of the CLI.
- Codex commands live outside the repository (under the user's home directory), so shipping an automated configurator that both scaffolds the prompts and keeps them refreshed via `openspec update` eliminates error-prone manual steps and keeps OpenSpec instructions synchronized across assistants.
## What Changes
- Add Codex to the `openspec init` tool picker with the same "already configured" detection we use for other editors, wiring an implementation that writes managed Markdown prompts directly to Codex's global directory (`~/.codex/prompts` or `$CODEX_HOME/prompts`) with OpenSpec marker blocks.
- Produce three Codex prompt files—`openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`—whose content mirrors the shared slash-command templates while using YAML frontmatter (`description` and `argument-hint` fields) and `$ARGUMENTS` to capture all arguments as a single string (matching the GitHub Copilot pattern and official Codex specification).
- Produce three Codex prompt files—`openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`—whose content mirrors the shared slash-command templates while adapting to Codex's numbered argument placeholders (e.g., `$1` for the change identifier or follow-up question text).
- Document Codex's global-only discovery and that OpenSpec writes prompts directly to `~/.codex/prompts` (or `$CODEX_HOME/prompts`).
- Teach `openspec update` to refresh existing Codex prompts in-place (and only when they already exist) in the global directory, updating both frontmatter and body.
- Teach `openspec update` to refresh existing Codex prompts in-place (and only when they already exist) in the global directory.
- Document Codex support alongside other slash-command integrations and add regression coverage that exercises init/update behaviour against a temporary global prompts directory via `CODEX_HOME`.
## Impact
+11 -44
View File
@@ -20,29 +20,14 @@ export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
}
protected getFrontmatter(id: SlashCommandId): string | undefined {
// Codex supports YAML frontmatter with description and argument-hint fields,
// plus $ARGUMENTS to capture all arguments as a single string.
const frontmatter: Record<SlashCommandId, string> = {
proposal: `---
description: Scaffold a new OpenSpec change and validate strictly.
argument-hint: request or feature description
---
$ARGUMENTS`,
apply: `---
description: Implement an approved OpenSpec change and keep tasks in sync.
argument-hint: change-id
---
$ARGUMENTS`,
archive: `---
description: Archive a deployed OpenSpec change and update specs.
argument-hint: change-id
---
$ARGUMENTS`,
// Codex does not use YAML front matter. Provide a heading-style
// preface that captures the first numbered placeholder `$1`.
const headers: Record<SlashCommandId, string> = {
proposal: "Request: $1",
apply: "Change ID: $1",
archive: "Change ID: $1",
};
return frontmatter[id];
return headers[id];
}
private getGlobalPromptsDir(): string {
@@ -64,11 +49,11 @@ $ARGUMENTS`,
await FileSystemUtils.createDirectory(path.dirname(filePath));
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateFullFile(filePath, target.id, body);
await this.updateBody(filePath, body);
} else {
const frontmatter = this.getFrontmatter(target.id);
const header = this.getFrontmatter(target.id);
const sections: string[] = [];
if (frontmatter) sections.push(frontmatter.trim());
if (header) sections.push(header.trim());
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n");
}
@@ -85,31 +70,13 @@ $ARGUMENTS`,
const filePath = path.join(promptsDir, path.basename(target.path));
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateFullFile(filePath, target.id, body);
await this.updateBody(filePath, body);
updated.push(target.path);
}
}
return updated;
}
// Update both frontmatter and body in an existing file
private async updateFullFile(filePath: string, id: SlashCommandId, body: string): Promise<void> {
const content = await FileSystemUtils.readFile(filePath);
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
if (startIndex === -1) {
throw new Error(`Missing OpenSpec start marker in ${filePath}`);
}
// Replace everything before the start marker with the new frontmatter
const frontmatter = this.getFrontmatter(id);
const sections: string[] = [];
if (frontmatter) sections.push(frontmatter.trim());
sections.push(`${OPENSPEC_MARKERS.start}\n${body}\n${OPENSPEC_MARKERS.end}`);
await FileSystemUtils.writeFile(filePath, sections.join("\n") + "\n");
}
// Resolve to the global prompts location for configuration detection
resolveAbsolutePath(_projectPath: string, id: SlashCommandId): string {
const promptsDir = this.getGlobalPromptsDir();
+3 -9
View File
@@ -339,22 +339,16 @@ describe('InitCommand', () => {
expect(await fileExists(archivePath)).toBe(true);
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('argument-hint: request or feature description');
expect(proposalContent).toContain('$ARGUMENTS');
expect(proposalContent).toContain('Request: $1');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
const applyContent = await fs.readFile(applyPath, 'utf-8');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('argument-hint: change-id');
expect(applyContent).toContain('$ARGUMENTS');
expect(applyContent).toContain('Change ID: $1');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(archivePath, 'utf-8');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('argument-hint: change-id');
expect(archiveContent).toContain('$ARGUMENTS');
expect(archiveContent).toContain('Change ID: $1');
expect(archiveContent).toContain('openspec archive <id> --yes');
});
+3 -6
View File
@@ -262,7 +262,7 @@ Old body
'.codex/prompts/openspec-apply.md'
);
await fs.mkdir(path.dirname(codexPath), { recursive: true });
const initialContent = `---\ndescription: Old description\nargument-hint: old-hint\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
const initialContent = `Change ID: $1\n<!-- OPENSPEC:START -->\nOld body\n<!-- OPENSPEC:END -->`;
await fs.writeFile(codexPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
@@ -270,12 +270,9 @@ Old body
await updateCommand.execute(testDir);
const updated = await fs.readFile(codexPath, 'utf-8');
expect(updated).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(updated).toContain('argument-hint: change-id');
expect(updated).toContain('$ARGUMENTS');
expect(updated).toContain('Change ID: $1');
expect(updated).toContain('Work through tasks sequentially');
expect(updated).not.toContain('Old body');
expect(updated).not.toContain('Old description');
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
@@ -295,7 +292,7 @@ Old body
await fs.mkdir(path.dirname(codexApply), { recursive: true });
await fs.writeFile(
codexApply,
'---\ndescription: Old\nargument-hint: old\n---\n\n$ARGUMENTS\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
'Change ID: $1\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
);
await updateCommand.execute(testDir);