Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 7157a37a39 docs(specs): update Cline config locations in cli-init and cli-update specs
- cli-init: Changed Cline config from root CLINE.md to .clinerules/openspec-rules.md
- cli-init: Fixed slash commands path from .clinerules/ to .clinerules/workflows/
- cli-update: Fixed slash commands path from .clinerules/ to .clinerules/workflows/
- cli-update: Added scenario for updating .clinerules/openspec-rules.md

These changes align the specs with the implementation that follows Cline's
convention for rules files while keeping workflows separate.
2025-11-25 12:22:55 +11:00
Tabish Bidiwale 50f6c254d6 fix(cline): use .clinerules/openspec-rules.md, keep root AGENTS.md
Based on PR feedback:
- Changed Cline config location from .clinerules/AGENTS.md to
  .clinerules/openspec-rules.md (matches Cline convention for rules files)
- Removed logic that deleted root AGENTS.md when Cline was selected
  (preserves compatibility when user selects multiple tools like
  CLINE + Codex, and prepares for upcoming AGENTS.md support in Cline)
- Updated tests to reflect new file location
2025-11-25 12:14:05 +11:00
Tabish Bidiwale b085c17945 Merge branch 'main' into fix/move-CLINE.md-to-rules 2025-11-13 23:05:37 +11:00
537e6078b7 Fix Cline: use workflows instead of rules for slash commands (#283)
* fix(cline): use workflows instead of rules for slash commands

- Update ClineSlashCommandConfigurator to use .clinerules/workflows/ paths
- Update tests to expect correct workflow file locations
- Update README.md to reflect workflows instead of rules
- Fixes Cline integration to match Cline's architecture per their blog post

* Adds spec for fix-cline-workflows-implementation

---------

Co-authored-by: didier <didier.boff@axess.fr>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2025-11-13 23:03:51 +11:00
Tabish Bidiwale 5439ab0833 Document Gemini CLI slash updates (#301) 2025-11-11 22:14:56 +11:00
Larry HopeandTabish Bidiwale 9b6a763eb8 feat: Add Gemini CLI support with TOML-based slash commands (#256)
* feat: add Gemini CLI support with TOML-based slash commands

- Add GeminiSlashCommandConfigurator for .gemini/commands/openspec/
- Register Gemini CLI in AI_TOOLS config and slash command registry
- Generate TOML files with description and prompt fields
- Add comprehensive test coverage for Gemini CLI integration
- Update README to list Gemini CLI under Native Slash Commands
- Remove Gemini CLI from AGENTS.md compatible list (now native)

Implements GitHub issue #248

* [add]

* feat: address PR feedback - remove changeset and add update test

- Remove .changeset/add-gemini-cli-support.md as requested by maintainer
- Add test for Gemini CLI TOML update/refresh path
- Test verifies that existing TOML files are properly updated when running init again

Addresses feedback from TabishB in PR #256

---------

Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2025-11-11 21:26:44 +11:00
didier 235afac85e Unlink AGENTS.md at the root when Cline is used 2025-11-05 19:12:12 +01:00
didier 5f0c5909fb Move CLINE.md to .clinerules 2025-11-05 18:22:23 +01:00
github-actions[bot]andTabish Bidiwale d32e50fe36 chore(release): version packages (#271)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-11-04 19:39:29 +11:00
Tabish Bidiwale 8386b91a71 Add changeset for AI assistants support and configuration improvements (#270) 2025-11-03 11:20:27 +11:00
8f9c3c7d0b feat: add Qwen Code support with slash command integration (#250)
* feat: add Qwen Code support with slash command integration

- Add QwenSlashCommandConfigurator for .qwen/commands/ structure
- Add QwenConfigurator to main registry
- Update README.md to include Qwen Code in supported tools list
- Register Qwen in both slash command and main tool registries
- Implement proper YAML frontmatter for Qwen command files
- Follow OpenSpec's established patterns for AI tool integration

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs: add docstrings and fix review comments for Qwen Code support

- Add comprehensive JSDoc comments to Qwen configurator files
- Add Qwen Code entry to README_CN.md support table
- Fix file ending newline in src/core/configurators/qwen.ts
- Address CodeRabbit review suggestions for improved documentation coverage

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: add language identifier to markdown code block in supportQwen.md

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address CodeRabbit review comments - parameter naming fix

- Fix unused parameter naming convention (_openspecDir)
- Keep only necessary changes for Qwen Code support

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore: add personal notes files to .gitignore

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore: remove personal notes files from git tracking

- Remove README_CN.md and supportQwen.md from git tracking
- These files are now ignored via .gitignore
- Keep only necessary files for the OpenSpec project

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: remove duplicate JSDoc comment in QwenConfigurator

- Remove duplicate documentation comment for configure method
- Keep only the updated comment that correctly documents the unused parameter

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test: cover qwen configurators

* chore: revert gitignore changes

* test: extend qwen init coverage

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-11-03 11:03:39 +11:00
Tabish Bidiwale 9cdb0743f2 feat: add $ARGUMENTS support to apply slash command (#244)
This change adds the $ARGUMENTS property to the OpenCode apply slash command template, allowing users to pass arguments when invoking the apply command. The template now includes instructions for the agent to find and implement the change proposal, with guidance to ask for clarification when ambiguous.
2025-11-02 23:12:02 +11:00
22 changed files with 663 additions and 62 deletions
+1 -1
View File
@@ -149,4 +149,4 @@ CLAUDE.md
.DS_Store
# Pnpm
.pnpm-store/
.pnpm-store/
+16
View File
@@ -1,5 +1,21 @@
# @fission-ai/openspec
## 0.14.0
### Minor Changes
- 8386b91: Add support for new AI assistants and configuration improvements
- feat: add Qwen Code support with slash command integration
- feat: add $ARGUMENTS support to apply slash command for dynamic variable passing
- feat: add Qoder CLI support to configuration and documentation
- feat: add CoStrict AI assistant support
- fix: recreate missing openspec template files in extend mode
- fix: prevent false 'already configured' detection for tools
- fix: use change-id as fallback title instead of "Untitled Change"
- docs: add guidance for populating project-level context
- docs: add Crush to supported AI tools in README
## 0.13.0
### Minor Changes
+4 -2
View File
@@ -94,9 +94,10 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
| **Cline** | Rules in `.clinerules/` directory (`.clinerules/openspec-*.md`) |
| **Cline** | Workflows in `.clinerules/workflows/` directory (`.clinerules/workflows/openspec-*.md`) |
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |
| **Factory Droid** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.factory/commands/`) |
| **Gemini CLI** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.gemini/commands/openspec/`) |
| **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) |
@@ -105,6 +106,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
@@ -114,7 +116,7 @@ These tools automatically read workflow instructions from `openspec/AGENTS.md`.
| Tools |
|-------|
| Amp • Jules • Gemini CLI • Others |
| Amp • Jules • Others |
### Install & Initialize
@@ -0,0 +1,13 @@
## Why
The Cline implementation was architecturally incorrect. According to Cline's official documentation, Cline uses workflows for on-demand automation and rules for behavioral guidelines. The OpenSpec slash commands are procedural workflows (scaffold → implement → archive), not behavioral rules, so they should be placed in `.clinerules/workflows/` instead of `.clinerules/`.
## What Changes
- Update ClineSlashCommandConfigurator to use `.clinerules/workflows/` paths instead of `.clinerules/` paths
- Update all tests to expect the correct workflow file locations
- Update README.md documentation to reflect workflows instead of rules
- **BREAKING**: Existing Cline users will need to re-run `openspec init` to get the corrected workflow files
## Impact
- Affected specs: cli-init (corrected Cline workflow paths)
- Affected code: `src/core/configurators/slash/cline.ts`, test files, README.md
- Modified files: `.clinerules/workflows/openspec-*.md` (moved from `.clinerules/openspec-*.md`)
@@ -0,0 +1,11 @@
# Delta for CLI Init
## MODIFIED Requirements
### Requirement: Slash Command Configuration
#### Scenario: Generating slash commands for Cline
- **WHEN** the user selects Cline during initialization
- **THEN** create `.clinerules/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
@@ -0,0 +1,13 @@
## 1. Update ClineSlashCommandConfigurator
- [x] Change FILE_PATHS in `src/core/configurators/slash/cline.ts` from `.clinerules/openspec-*.md` to `.clinerules/workflows/openspec-*.md`
## 2. Update Tests
- [x] Update "should refresh existing Cline rule files" test in `test/core/update.test.ts` to use workflow paths
- [x] Update "should create Cline rule files with templates" test in `test/core/init.test.ts` to use workflow paths
## 3. Update Documentation
- [x] Update README.md table to show "Workflows in `.clinerules/workflows/` directory" for Cline
## 4. Validate Changes
- [x] Ensure all tests pass with the new paths
- [x] Verify the change follows OpenSpec conventions
+9 -2
View File
@@ -73,7 +73,7 @@ The command SHALL properly configure selected AI tools with OpenSpec-specific in
#### Scenario: Configuring Cline
- **WHEN** Cline is selected
- **THEN** create or update `CLINE.md` in the project root directory (not inside openspec/)
- **THEN** create or update `.clinerules/openspec-rules.md` (following Cline's convention for rules files)
- **AND** populate the managed block with a short stub that points teammates to `@/openspec/AGENTS.md`
#### Scenario: Creating new CLAUDE.md
@@ -174,7 +174,7 @@ The init command SHALL generate slash command files for supported editors using
#### Scenario: Generating slash commands for Cline
- **WHEN** the user selects Cline during initialization
- **THEN** create `.clinerules/openspec-proposal.md`, `.clinerules/openspec-apply.md`, and `.clinerules/openspec-archive.md`
- **THEN** create `.clinerules/workflows/openspec-proposal.md`, `.clinerules/workflows/openspec-apply.md`, and `.clinerules/workflows/openspec-archive.md`
- **AND** populate each file from shared templates so command text matches other tools
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
@@ -231,6 +231,13 @@ The init command SHALL generate slash command files for supported editors using
- **AND** wrap the shared template body with OpenSpec markers so `openspec update` can refresh the content
- **AND** each template includes instructions for the relevant OpenSpec workflow stage
#### Scenario: Generating slash commands for Gemini CLI
- **WHEN** the user selects Gemini CLI during initialization
- **THEN** create `.gemini/commands/openspec/proposal.toml`, `.gemini/commands/openspec/apply.toml`, and `.gemini/commands/openspec/archive.toml`
- **AND** populate each file as TOML that sets a stage-specific `description = "<summary>"` and a multi-line `prompt = """` block with the shared OpenSpec template
- **AND** wrap the OpenSpec managed markers (`<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->`) inside the `prompt` value so `openspec update` can safely refresh the body between markers without touching the TOML framing
- **AND** ensure the slash-command copy matches the existing proposal/apply/archive templates used by other tools
### Requirement: Non-Interactive Mode
The command SHALL support non-interactive operation through command-line options for automation and CI/CD use cases.
+12 -1
View File
@@ -63,11 +63,16 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Cline
- **WHEN** `.clinerules/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **WHEN** `.clinerules/workflows/` contains `openspec-proposal.md`, `openspec-apply.md`, and `openspec-archive.md`
- **THEN** refresh each file using shared templates
- **AND** include Cline-specific Markdown heading frontmatter
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating Cline rules file
- **WHEN** `.clinerules/openspec-rules.md` exists
- **THEN** refresh the managed block to point teammates to `@/openspec/AGENTS.md`
- **AND** preserve any content outside the managed markers
#### 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
@@ -117,6 +122,12 @@ The update command SHALL refresh existing slash command files for configured too
- **AND** update only the OpenSpec-managed block between markers
- **AND** ensure templates include instructions for the relevant workflow stage
#### Scenario: Updating slash commands for Gemini CLI
- **WHEN** `.gemini/commands/openspec/` contains `proposal.toml`, `apply.toml`, and `archive.toml`
- **THEN** refresh the body of each file using the shared proposal/apply/archive templates
- **AND** replace only the content between `<!-- OPENSPEC:START -->` and `<!-- OPENSPEC:END -->` markers inside the `prompt = """` block so the TOML framing (`description`, `prompt`) stays intact
- **AND** skip creating any missing `.toml` files during update; only pre-existing Gemini commands are refreshed
#### Scenario: Missing slash command file
- **WHEN** a tool lacks a slash command file
- **THEN** do not create a new file during update
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.13.0",
"version": "0.14.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
@@ -59,7 +59,7 @@
"@changesets/cli": "^2.27.7",
"@types/node": "^24.2.0",
"@vitest/ui": "^3.2.4",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4"
},
"dependencies": {
+5 -5
View File
@@ -37,8 +37,8 @@ importers:
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4)
typescript:
specifier: ^5.9.2
version: 5.9.2
specifier: ^5.9.3
version: 5.9.3
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/node@24.2.0)(@vitest/ui@3.2.4)
@@ -1115,8 +1115,8 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
@@ -2223,7 +2223,7 @@ snapshots:
type-fest@0.21.3: {}
typescript@5.9.2: {}
typescript@5.9.3: {}
undici-types@7.10.0: {}
+2
View File
@@ -25,6 +25,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI' },
{ 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' },
@@ -32,5 +33,6 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code' },
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
];
+3 -3
View File
@@ -6,13 +6,13 @@ import { OPENSPEC_MARKERS } from '../config.js';
export class ClineConfigurator implements ToolConfigurator {
name = 'Cline';
configFileName = 'CLINE.md';
configFileName = '.clinerules/openspec-rules.md';
isAvailable = true;
async configure(projectPath: string, openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getClineTemplate();
const content = TemplateManager.getAgentsStandardTemplate();
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
+47
View File
@@ -0,0 +1,47 @@
/**
* Qwen Code configurator for OpenSpec integration.
* This class handles the configuration of Qwen Code as an AI tool within OpenSpec.
*
* @implements {ToolConfigurator}
*/
import path from 'path';
import { ToolConfigurator } from './base.js';
import { FileSystemUtils } from '../../utils/file-system.js';
import { TemplateManager } from '../templates/index.js';
import { OPENSPEC_MARKERS } from '../config.js';
/**
* QwenConfigurator class provides integration with Qwen Code
* by creating and managing the necessary configuration files.
* Currently configures the QWEN.md file with OpenSpec instructions.
*/
export class QwenConfigurator implements ToolConfigurator {
/** Display name for the Qwen Code tool */
name = 'Qwen Code';
/** Configuration file name for Qwen Code */
configFileName = 'QWEN.md';
/** Availability status for the Qwen Code tool */
isAvailable = true;
/**
* Configures the Qwen Code integration by creating or updating the QWEN.md file
* with OpenSpec instructions and markers.
*
* @param {string} projectPath - The path to the project root
* @param {string} _openspecDir - The path to the openspec directory (unused)
* @returns {Promise<void>} A promise that resolves when configuration is complete
*/
async configure(projectPath: string, _openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getAgentsStandardTemplate();
await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
}
}
+3
View File
@@ -5,6 +5,7 @@ import { CodeBuddyConfigurator } from './codebuddy.js';
import { CostrictConfigurator } from './costrict.js';
import { QoderConfigurator } from './qoder.js';
import { AgentsStandardConfigurator } from './agents.js';
import { QwenConfigurator } from './qwen.js';
export class ToolRegistry {
private static tools: Map<string, ToolConfigurator> = new Map();
@@ -16,6 +17,7 @@ export class ToolRegistry {
const costrictConfigurator = new CostrictConfigurator();
const qoderConfigurator = new QoderConfigurator();
const agentsConfigurator = new AgentsStandardConfigurator();
const qwenConfigurator = new QwenConfigurator();
// Register with the ID that matches the checkbox value
this.tools.set('claude', claudeConfigurator);
this.tools.set('cline', clineConfigurator);
@@ -23,6 +25,7 @@ export class ToolRegistry {
this.tools.set('costrict', costrictConfigurator);
this.tools.set('qoder', qoderConfigurator);
this.tools.set('agents', agentsConfigurator);
this.tools.set('qwen', qwenConfigurator);
}
static register(tool: ToolConfigurator): void {
+3 -3
View File
@@ -2,9 +2,9 @@ import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.clinerules/openspec-proposal.md',
apply: '.clinerules/openspec-apply.md',
archive: '.clinerules/openspec-archive.md'
proposal: '.clinerules/workflows/openspec-proposal.md',
apply: '.clinerules/workflows/openspec-apply.md',
archive: '.clinerules/workflows/openspec-archive.md'
};
export class ClineSlashCommandConfigurator extends SlashCommandConfigurator {
+83
View File
@@ -0,0 +1,83 @@
import { FileSystemUtils } from '../../../utils/file-system.js';
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId, TemplateManager } from '../../templates/index.js';
import { OPENSPEC_MARKERS } from '../../config.js';
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.gemini/commands/openspec/proposal.toml',
apply: '.gemini/commands/openspec/apply.toml',
archive: '.gemini/commands/openspec/archive.toml'
};
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 GeminiSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'gemini';
readonly isAvailable = true;
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
protected getFrontmatter(_id: SlashCommandId): string | undefined {
// TOML doesn't use separate frontmatter - it's all in one structure
return undefined;
}
// Override to generate TOML format with markers inside the prompt field
async generateAll(projectPath: string, _openspecDir: string): Promise<string[]> {
const createdOrUpdated: string[] = [];
for (const target of this.getTargets()) {
const body = this.getBody(target.id);
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateBody(filePath, body);
} else {
const tomlContent = this.generateTOML(target.id, body);
await FileSystemUtils.writeFile(filePath, tomlContent);
}
createdOrUpdated.push(target.path);
}
return createdOrUpdated;
}
private generateTOML(id: SlashCommandId, body: string): string {
const description = DESCRIPTIONS[id];
// TOML format with triple-quoted string for multi-line prompt
// Markers are inside the prompt value
return `description = "${description}"
prompt = """
${OPENSPEC_MARKERS.start}
${body}
${OPENSPEC_MARKERS.end}
"""
`;
}
// Override updateBody to handle TOML format
protected async updateBody(filePath: string, body: string): Promise<void> {
const content = await FileSystemUtils.readFile(filePath);
const startIndex = content.indexOf(OPENSPEC_MARKERS.start);
const endIndex = content.indexOf(OPENSPEC_MARKERS.end);
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
throw new Error(`Missing OpenSpec markers in ${filePath}`);
}
const before = content.slice(0, startIndex + OPENSPEC_MARKERS.start.length);
const after = content.slice(endIndex);
const updatedContent = `${before}\n${body}\n${after}`;
await FileSystemUtils.writeFile(filePath, updatedContent);
}
}
+6 -1
View File
@@ -22,7 +22,12 @@ The user has requested the following change proposal. Use the openspec instructi
apply: `---
agent: build
description: Implement an approved OpenSpec change and keep tasks in sync.
---`,
---
The user has requested to implement the following change proposal. Find the change proposal and follow the instructions below. If you're not sure or if ambiguous, ask for clarification from the user.
<UserRequest>
$ARGUMENTS
</UserRequest>
`,
archive: `---
agent: build
description: Archive a deployed OpenSpec change and update specs.
+80
View File
@@ -0,0 +1,80 @@
/**
* Qwen slash command configurator for OpenSpec integration.
* This class handles the generation of Qwen-specific slash command files
* in the .qwen/commands directory structure.
*
* @implements {SlashCommandConfigurator}
*/
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';
/**
* Mapping of slash command IDs to their corresponding file paths in .qwen/commands directory.
* @type {Record<SlashCommandId, string>}
*/
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.qwen/commands/openspec-proposal.md',
apply: '.qwen/commands/openspec-apply.md',
archive: '.qwen/commands/openspec-archive.md'
};
/**
* YAML frontmatter definitions for Qwen command files.
* These provide metadata for each slash command to ensure proper recognition by Qwen Code.
* @type {Record<SlashCommandId, string>}
*/
const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---`,
apply: `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Implement an approved OpenSpec change and keep tasks in sync.
---`,
archive: `---
name: /openspec-archive
id: openspec-archive
category: OpenSpec
description: Archive a deployed OpenSpec change and update specs.
---`
};
/**
* QwenSlashCommandConfigurator class provides integration with Qwen Code
* by creating the necessary slash command files in the .qwen/commands directory.
*
* The slash commands include:
* - /openspec-proposal: Create an OpenSpec change proposal
* - /openspec-apply: Apply an approved OpenSpec change
* - /openspec-archive: Archive a deployed OpenSpec change
*/
export class QwenSlashCommandConfigurator extends SlashCommandConfigurator {
/** Unique identifier for the Qwen tool */
readonly toolId = 'qwen';
/** Availability status for the Qwen tool */
readonly isAvailable = true;
/**
* Returns the relative file path for a given slash command ID.
* @param {SlashCommandId} id - The slash command identifier
* @returns {string} The relative path to the command file
*/
protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}
/**
* Returns the YAML frontmatter for a given slash command ID.
* @param {SlashCommandId} id - The slash command identifier
* @returns {string} The YAML frontmatter string
*/
protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
+6
View File
@@ -10,10 +10,12 @@ import { CodexSlashCommandConfigurator } from './codex.js';
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
import { FactorySlashCommandConfigurator } from './factory.js';
import { GeminiSlashCommandConfigurator } from './gemini.js';
import { AuggieSlashCommandConfigurator } from './auggie.js';
import { ClineSlashCommandConfigurator } from './cline.js';
import { CrushSlashCommandConfigurator } from './crush.js';
import { CostrictSlashCommandConfigurator } from './costrict.js';
import { QwenSlashCommandConfigurator } from './qwen.js';
export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -30,10 +32,12 @@ export class SlashCommandRegistry {
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
const amazonQ = new AmazonQSlashCommandConfigurator();
const factory = new FactorySlashCommandConfigurator();
const gemini = new GeminiSlashCommandConfigurator();
const auggie = new AuggieSlashCommandConfigurator();
const cline = new ClineSlashCommandConfigurator();
const crush = new CrushSlashCommandConfigurator();
const costrict = new CostrictSlashCommandConfigurator();
const qwen = new QwenSlashCommandConfigurator();
this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -46,10 +50,12 @@ export class SlashCommandRegistry {
this.configurators.set(githubCopilot.toolId, githubCopilot);
this.configurators.set(amazonQ.toolId, amazonQ);
this.configurators.set(factory.toolId, factory);
this.configurators.set(gemini.toolId, gemini);
this.configurators.set(auggie.toolId, auggie);
this.configurators.set(cline.toolId, cline);
this.configurators.set(crush.toolId, crush);
this.configurators.set(costrict.toolId, costrict);
this.configurators.set(qwen.toolId, qwen);
}
static register(configurator: SlashCommandConfigurator): void {
+4
View File
@@ -126,6 +126,10 @@ export class FileSystemUtils {
return await fs.readFile(filePath, 'utf-8');
}
static async deleteFile(filePath: string): Promise<void> {
await fs.unlink(filePath);
}
static async updateFileWithMarkers(
filePath: string,
content: string,
+154 -27
View File
@@ -10,7 +10,7 @@ type SelectionQueue = string[][];
let selectionQueue: SelectionQueue = [];
const mockPrompt = vi.fn(async () => {
const mockPrompt = vi.fn(async (options: any) => {
if (selectionQueue.length === 0) {
throw new Error('No queued selections provided to init prompt.');
}
@@ -136,38 +136,21 @@ describe('InitCommand', () => {
expect(updatedContent).toContain('Custom instructions here');
});
it('should create CLINE.md when Cline is selected', async () => {
it('should create .clinerules/openspec-rules.md when Cline is selected', async () => {
queueSelections('cline', DONE);
await initCommand.execute(testDir);
const clinePath = path.join(testDir, 'CLINE.md');
expect(await fileExists(clinePath)).toBe(true);
const clineRulesPath = path.join(testDir, '.clinerules', 'openspec-rules.md');
expect(await fileExists(clineRulesPath)).toBe(true);
const content = await fs.readFile(clinePath, 'utf-8');
const content = await fs.readFile(clineRulesPath, 'utf-8');
expect(content).toContain('<!-- OPENSPEC:START -->');
expect(content).toContain("@/openspec/AGENTS.md");
expect(content).toContain('OpenSpec Instructions');
expect(content).toContain('openspec update');
expect(content).toContain('<!-- OPENSPEC:END -->');
});
it('should update existing CLINE.md with markers', async () => {
queueSelections('cline', DONE);
const clinePath = path.join(testDir, 'CLINE.md');
const existingContent =
'# My Cline Rules\nCustom Cline instructions here';
await fs.writeFile(clinePath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(clinePath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom Cline instructions here');
});
it('should create Windsurf workflows when Windsurf is selected', async () => {
queueSelections('windsurf', DONE);
@@ -305,6 +288,73 @@ describe('InitCommand', () => {
expect(archiveContent).toContain('openspec list --specs');
});
it('should create Gemini CLI TOML files when selected', async () => {
queueSelections('gemini', DONE);
await initCommand.execute(testDir);
const geminiProposal = path.join(
testDir,
'.gemini/commands/openspec/proposal.toml'
);
const geminiApply = path.join(
testDir,
'.gemini/commands/openspec/apply.toml'
);
const geminiArchive = path.join(
testDir,
'.gemini/commands/openspec/archive.toml'
);
expect(await fileExists(geminiProposal)).toBe(true);
expect(await fileExists(geminiApply)).toBe(true);
expect(await fileExists(geminiArchive)).toBe(true);
const proposalContent = await fs.readFile(geminiProposal, 'utf-8');
expect(proposalContent).toContain('description = "Scaffold a new OpenSpec change and validate strictly."');
expect(proposalContent).toContain('prompt = """');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');
const applyContent = await fs.readFile(geminiApply, 'utf-8');
expect(applyContent).toContain('description = "Implement an approved OpenSpec change and keep tasks in sync."');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(geminiArchive, 'utf-8');
expect(archiveContent).toContain('description = "Archive a deployed OpenSpec change and update specs."');
expect(archiveContent).toContain('openspec archive <id>');
});
it('should update existing Gemini CLI TOML files with refreshed content', async () => {
queueSelections('gemini', DONE);
await initCommand.execute(testDir);
const geminiProposal = path.join(
testDir,
'.gemini/commands/openspec/proposal.toml'
);
// Modify the file to simulate user customization
const originalContent = await fs.readFile(geminiProposal, 'utf-8');
const modifiedContent = originalContent.replace(
'<!-- OPENSPEC:START -->',
'<!-- OPENSPEC:START -->\nCustom instruction added by user\n'
);
await fs.writeFile(geminiProposal, modifiedContent);
// Run init again to test update/refresh path
queueSelections('gemini', DONE);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(geminiProposal, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('**Guardrails**');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).not.toContain('Custom instruction added by user');
});
it('should create OpenCode slash command files with templates', async () => {
queueSelections('opencode', DONE);
@@ -349,22 +399,87 @@ describe('InitCommand', () => {
expect(archiveContent).toContain('openspec list --specs');
});
it('should create Cline rule files with templates', async () => {
it('should create Qwen configuration and slash command files with templates', async () => {
queueSelections('qwen', DONE);
await initCommand.execute(testDir);
const qwenConfigPath = path.join(testDir, 'QWEN.md');
const proposalPath = path.join(
testDir,
'.qwen/commands/openspec-proposal.md'
);
const applyPath = path.join(
testDir,
'.qwen/commands/openspec-apply.md'
);
const archivePath = path.join(
testDir,
'.qwen/commands/openspec-archive.md'
);
expect(await fileExists(qwenConfigPath)).toBe(true);
expect(await fileExists(proposalPath)).toBe(true);
expect(await fileExists(applyPath)).toBe(true);
expect(await fileExists(archivePath)).toBe(true);
const qwenConfigContent = await fs.readFile(qwenConfigPath, 'utf-8');
expect(qwenConfigContent).toContain('<!-- OPENSPEC:START -->');
expect(qwenConfigContent).toContain("@/openspec/AGENTS.md");
expect(qwenConfigContent).toContain('<!-- OPENSPEC:END -->');
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
expect(proposalContent).toContain('name: /openspec-proposal');
expect(proposalContent).toContain('category: OpenSpec');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
const applyContent = await fs.readFile(applyPath, 'utf-8');
expect(applyContent).toContain('name: /openspec-apply');
expect(applyContent).toContain('category: OpenSpec');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('Work through tasks sequentially');
const archiveContent = await fs.readFile(archivePath, 'utf-8');
expect(archiveContent).toContain('name: /openspec-archive');
expect(archiveContent).toContain('category: OpenSpec');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('openspec archive <id>');
});
it('should update existing QWEN.md with markers', async () => {
queueSelections('qwen', DONE);
const qwenPath = path.join(testDir, 'QWEN.md');
const existingContent = '# My Qwen Instructions\nCustom instructions here';
await fs.writeFile(qwenPath, existingContent);
await initCommand.execute(testDir);
const updatedContent = await fs.readFile(qwenPath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom instructions here');
});
it('should create Cline workflow files with templates', async () => {
queueSelections('cline', DONE);
await initCommand.execute(testDir);
const clineProposal = path.join(
testDir,
'.clinerules/openspec-proposal.md'
'.clinerules/workflows/openspec-proposal.md'
);
const clineApply = path.join(
testDir,
'.clinerules/openspec-apply.md'
'.clinerules/workflows/openspec-apply.md'
);
const clineArchive = path.join(
testDir,
'.clinerules/openspec-archive.md'
'.clinerules/workflows/openspec-archive.md'
);
expect(await fileExists(clineProposal)).toBe(true);
@@ -688,6 +803,18 @@ describe('InitCommand', () => {
expect(claudeChoice.configured).toBe(true);
});
it('should mark Qwen as already configured during extend mode', async () => {
queueSelections('qwen', DONE, 'qwen', DONE);
await initCommand.execute(testDir);
await initCommand.execute(testDir);
const secondRunArgs = mockPrompt.mock.calls[1][0];
const qwenChoice = secondRunArgs.choices.find(
(choice: any) => choice.value === 'qwen'
);
expect(qwenChoice.configured).toBe(true);
});
it('should preselect Kilo Code when workflows already exist', async () => {
queueSelections('kilocode', DONE, 'kilocode', DONE);
await initCommand.execute(testDir);
+186 -15
View File
@@ -73,6 +73,41 @@ More content after.`;
consoleSpy.mockRestore();
});
it('should update only existing QWEN.md file', async () => {
const qwenPath = path.join(testDir, 'QWEN.md');
const initialContent = `# Qwen Instructions
Some existing content.
<!-- OPENSPEC:START -->
Old OpenSpec content
<!-- OPENSPEC:END -->
More notes here.`;
await fs.writeFile(qwenPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updatedContent = await fs.readFile(qwenPath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('Some existing content.');
expect(updatedContent).toContain('More notes here.');
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 AI tool files: QWEN.md');
consoleSpy.mockRestore();
});
it('should refresh existing Claude slash command files', async () => {
const proposalPath = path.join(
testDir,
@@ -114,6 +149,87 @@ Old slash content
consoleSpy.mockRestore();
});
it('should refresh existing Qwen slash command files', async () => {
const applyPath = path.join(
testDir,
'.qwen/commands/openspec-apply.md'
);
await fs.mkdir(path.dirname(applyPath), { recursive: true });
const initialContent = `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Old description
---
<!-- OPENSPEC:START -->
Old body
<!-- OPENSPEC:END -->`;
await fs.writeFile(applyPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(applyPath, 'utf-8');
expect(updated).toContain('name: /openspec-apply');
expect(updated).toContain('category: OpenSpec');
expect(updated).toContain('<!-- OPENSPEC:START -->');
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: .qwen/commands/openspec-apply.md'
);
consoleSpy.mockRestore();
});
it('should not create missing Qwen slash command files on update', async () => {
const applyPath = path.join(
testDir,
'.qwen/commands/openspec-apply.md'
);
await fs.mkdir(path.dirname(applyPath), { recursive: true });
await fs.writeFile(
applyPath,
`---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Old description
---
<!-- OPENSPEC:START -->
Old content
<!-- OPENSPEC:END -->`
);
await updateCommand.execute(testDir);
const updatedApply = await fs.readFile(applyPath, 'utf-8');
expect(updatedApply).toContain('Work through tasks sequentially');
expect(updatedApply).not.toContain('Old content');
const proposalPath = path.join(
testDir,
'.qwen/commands/openspec-proposal.md'
);
const archivePath = path.join(
testDir,
'.qwen/commands/openspec-archive.md'
);
await expect(FileSystemUtils.fileExists(proposalPath)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(archivePath)).resolves.toBe(false);
});
it('should not create CLAUDE.md if it does not exist', async () => {
// Ensure CLAUDE.md does not exist
const claudePath = path.join(testDir, 'CLAUDE.md');
@@ -126,9 +242,16 @@ Old slash content
expect(fileExists).toBe(false);
});
it('should update only existing CLINE.md file', async () => {
// Create CLINE.md file with initial content
const clinePath = path.join(testDir, 'CLINE.md');
it('should not create QWEN.md if it does not exist', async () => {
const qwenPath = path.join(testDir, 'QWEN.md');
await updateCommand.execute(testDir);
await expect(FileSystemUtils.fileExists(qwenPath)).resolves.toBe(false);
});
it('should update only existing .clinerules/openspec-rules.md file', async () => {
// Create .clinerules/openspec-rules.md file with initial content
const clineRulesPath = path.join(testDir, '.clinerules', 'openspec-rules.md');
await fs.mkdir(path.dirname(clineRulesPath), { recursive: true });
const initialContent = `# Cline Rules
Some existing Cline rules here.
@@ -138,15 +261,15 @@ Old OpenSpec content
<!-- OPENSPEC:END -->
More rules after.`;
await fs.writeFile(clinePath, initialContent);
await fs.writeFile(clineRulesPath, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
// Execute update command
await updateCommand.execute(testDir);
// Check that CLINE.md was updated
const updatedContent = await fs.readFile(clinePath, 'utf-8');
// Check that .clinerules/openspec-rules.md was updated
const updatedContent = await fs.readFile(clineRulesPath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
@@ -160,26 +283,26 @@ More rules after.`;
'Updated OpenSpec instructions (openspec/AGENTS.md'
);
expect(logMessage).toContain('AGENTS.md (created)');
expect(logMessage).toContain('Updated AI tool files: CLINE.md');
expect(logMessage).toContain('Updated AI tool files: .clinerules/openspec-rules.md');
consoleSpy.mockRestore();
});
it('should not create CLINE.md if it does not exist', async () => {
// Ensure CLINE.md does not exist
const clinePath = path.join(testDir, 'CLINE.md');
it('should not create .clinerules/openspec-rules.md if it does not exist', async () => {
// Ensure .clinerules/openspec-rules.md does not exist
const clineRulesPath = path.join(testDir, '.clinerules', 'openspec-rules.md');
// Execute update command
await updateCommand.execute(testDir);
// Check that CLINE.md was not created
const fileExists = await FileSystemUtils.fileExists(clinePath);
// Check that .clinerules/openspec-rules.md was not created
const fileExists = await FileSystemUtils.fileExists(clineRulesPath);
expect(fileExists).toBe(false);
});
it('should refresh existing Cline rule files', async () => {
it('should refresh existing Cline workflow files', async () => {
const proposalPath = path.join(
testDir,
'.clinerules/openspec-proposal.md'
'.clinerules/workflows/openspec-proposal.md'
);
await fs.mkdir(path.dirname(proposalPath), { recursive: true });
const initialContent = `# OpenSpec: Proposal
@@ -209,7 +332,7 @@ Old slash content
);
expect(logMessage).toContain('AGENTS.md (created)');
expect(logMessage).toContain(
'Updated slash commands: .clinerules/openspec-proposal.md'
'Updated slash commands: .clinerules/workflows/openspec-proposal.md'
);
consoleSpy.mockRestore();
@@ -466,6 +589,54 @@ Old body
await expect(FileSystemUtils.fileExists(ghArchive)).resolves.toBe(false);
});
it('should refresh existing Gemini CLI TOML files without creating new ones', async () => {
const geminiProposal = path.join(
testDir,
'.gemini/commands/openspec/proposal.toml'
);
await fs.mkdir(path.dirname(geminiProposal), { recursive: true });
const initialContent = `description = "Scaffold a new OpenSpec change and validate strictly."
prompt = """
<!-- OPENSPEC:START -->
Old Gemini body
<!-- OPENSPEC:END -->
"""
`;
await fs.writeFile(geminiProposal, initialContent);
const consoleSpy = vi.spyOn(console, 'log');
await updateCommand.execute(testDir);
const updated = await fs.readFile(geminiProposal, 'utf-8');
expect(updated).toContain('description = "Scaffold a new OpenSpec change and validate strictly."');
expect(updated).toContain('prompt = """');
expect(updated).toContain('<!-- OPENSPEC:START -->');
expect(updated).toContain('**Guardrails**');
expect(updated).toContain('<!-- OPENSPEC:END -->');
expect(updated).not.toContain('Old Gemini body');
const geminiApply = path.join(
testDir,
'.gemini/commands/openspec/apply.toml'
);
const geminiArchive = path.join(
testDir,
'.gemini/commands/openspec/archive.toml'
);
await expect(FileSystemUtils.fileExists(geminiApply)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(geminiArchive)).resolves.toBe(false);
const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
'Updated slash commands: .gemini/commands/openspec/proposal.toml'
);
consoleSpy.mockRestore();
});
it('should refresh existing Factory slash commands', async () => {
const factoryPath = path.join(
testDir,