Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/restart-ide-hint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@fission-ai/openspec": patch
---

### Bug Fixes

- `openspec init` now suggests an IDE restart only when an IDE-resident tool such as Cursor, GitHub Copilot, Continue, or Cline was configured. CLI tools like Claude Code, Codex, and Gemini CLI no longer show the hint, since their commands work as soon as the files exist.
37 changes: 22 additions & 15 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,44 +34,51 @@ export interface AIToolOption {
globalSkillsDir?: string; // e.g., '.minimax' - /skills suffix, resolved from the user's home directory
detectionPaths?: string[]; // Override skillsDir for auto-detection; any path existing triggers detection
setupNote?: string; // Manual setup required before the tool picks up generated files; shown after init/update
// True when the tool's commands/skills are loaded by a long-running IDE/editor
// process that must be restarted to pick up newly written files; false/omitted
// for terminal CLIs that read them fresh each run (see #1067). Flagged by the
// tool's primary surface: a few tools ship both an IDE extension and a CLI
// (e.g. Continue, Amazon Q), and a per-tool boolean cannot tell which variant
// is installed — they are flagged as IDE since that is the common case.
requiresIdeRestart?: boolean;
}

export const AI_TOOLS: AIToolOption[] = [
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' },
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' },
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq', requiresIdeRestart: true },
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent', requiresIdeRestart: true },
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
{ name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline', requiresIdeRestart: true },
{ name: 'CodeArts', value: 'codeartsagent', available: true, successLabel: 'CodeArts', skillsDir: '.codeartsdoer' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.agents', legacySkillsDirs: ['.codex'], detectionPaths: ['.agents/skills', '.codex/skills'] },
{ name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'] },
{ name: 'Devin Desktop (formerly Windsurf)', value: 'devin', available: true, successLabel: 'Devin Desktop', skillsDir: '.devin', detectionPaths: ['.devin', '.windsurf'], requiresIdeRestart: true },
{ name: 'ForgeCode', value: 'forgecode', available: true, successLabel: 'ForgeCode', skillsDir: '.forge' },
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code', skillsDir: '.codebuddy' },
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue' },
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec' },
{ name: 'Continue', value: 'continue', available: true, successLabel: 'Continue (VS Code / JetBrains / Cli)', skillsDir: '.continue', requiresIdeRestart: true },
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec', requiresIdeRestart: true },
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush', skillsDir: '.crush' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' },
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor', requiresIdeRestart: true },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' },
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] },
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'], requiresIdeRestart: true },
{ name: 'Hermes Agent', value: 'hermes', available: true, successLabel: 'Hermes Agent', skillsDir: '.hermes', detectionPaths: ['.hermes', 'HERMES.md', '.hermes.md'], setupNote: "Hermes only loads skills from ~/.hermes/skills by default. Add this project's .hermes/skills directory to skills.external_dirs in ~/.hermes/config.yaml so Hermes picks up the generated OpenSpec skills." },
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
{ name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
{ name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie', requiresIdeRestart: true },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode', requiresIdeRestart: true },
{ name: 'Kimi Code', value: 'kimi', available: true, successLabel: 'Kimi Code', skillsDir: '.kimi-code', detectionPaths: ['.kimi-code', '.kimi'] },
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
{ name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro', requiresIdeRestart: true },
{ name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma', requiresIdeRestart: true },
{ name: 'MiniMax Code', value: 'minimax-code', available: true, successLabel: 'MiniMax Code', globalSkillsDir: '.minimax' },
{ name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' },
{ name: 'Oh My Pi', value: 'oh-my-pi', available: true, successLabel: 'Oh My Pi', skillsDir: '.omp' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder', requiresIdeRestart: true },
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
{ name: 'Rovo Dev CLI', value: 'rovodev', available: true, successLabel: 'Rovo Dev CLI', skillsDir: '.rovodev', detectionPaths: ['.rovodev/skills', '.rovodev'] },
{ name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo' },
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
{ name: 'Zoo Code', value: 'roocode', available: true, successLabel: 'Zoo Code', skillsDir: '.roo', requiresIdeRestart: true },
{ name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true },
{ name: 'ZCode', value: 'zcode', available: true, successLabel: 'ZCode', skillsDir: '.zcode' },
// Vendor-neutral target for assistants that read the shared `.agents` root.
// Detection keys off `.agents/skills` rather than the bare root: frameworks use
Expand Down
37 changes: 28 additions & 9 deletions src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ type ValidatedInitTool = {
skillsRoot: string;
isGlobalSkillTarget: boolean;
wasConfigured: boolean;
requiresIdeRestart?: boolean;
};

/**
Expand Down Expand Up @@ -788,6 +789,7 @@ export class InitCommand {
skillsRoot: isGlobalSkillTarget ? skillsPath : projectPath,
isGlobalSkillTarget,
wasConfigured: preState?.configured ?? false,
requiresIdeRestart: tool.requiresIdeRestart,
});
}

Expand Down Expand Up @@ -1012,8 +1014,8 @@ export class InitCommand {
projectPath: string,
tools: ValidatedInitTool[],
results: {
createdTools: typeof tools;
refreshedTools: typeof tools;
createdTools: ValidatedInitTool[];
refreshedTools: ValidatedInitTool[];
failedTools: Array<{ name: string; error: Error }>;
commandsSkipped: string[];
skillsInvocableCommandSkips: string[];
Expand Down Expand Up @@ -1274,16 +1276,33 @@ export class InitCommand {
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
console.log(`Feedback: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec/issues')}`);

// Restart instruction if any tools were configured and got a surface
// (when nothing was generated there is nothing a restart would pick up);
// only mention commands when commands were actually generated. Not "slash
// commands": Amazon Q's generated files are prompt-library entries invoked
// with @, so a restart line promising slash commands would be wrong for it.
if ((results.createdTools.length > 0 || results.refreshedTools.length > 0) && (commandsGenerated || skillsGenerated)) {
// Restart instruction only when at least one IDE/editor-resident tool
// actually received a generated surface. Two conditions, coupled to the SAME
// tool: (1) its commands/skills are loaded by a long-running editor process
// (CLI tools pick the files up immediately, so a restart line would be wrong
// for them — see #1067), and (2) a surface was actually generated for it
// under the active delivery (an IDE tool that generated nothing has nothing a
// restart would pick up, even if a co-configured CLI tool did generate).
// Wording follows what the IDE tool itself generated, not the global
// aggregate: it must not say "commands" when the IDE tool only got skills
// while a co-configured CLI tool got commands. Not "slash commands" either:
// Amazon Q's generated files are prompt-library entries invoked with @, so a
// restart line promising slash commands would be wrong for it.
const restartCommandsGenerated = successfulTools.some(
(tool) =>
tool.requiresIdeRestart &&
shouldGenerateCommandsForTool(tool.value, activeDelivery)
);
const restartSkillsGenerated = successfulTools.some(
(tool) =>
tool.requiresIdeRestart &&
shouldGenerateSkillsForTool(tool.value, activeDelivery)
);
if (restartCommandsGenerated || restartSkillsGenerated) {
console.log();
console.log(
chalk.white(
commandsGenerated
restartCommandsGenerated
? 'Restart your IDE for the new commands to take effect.'
: 'Restart your IDE for the new skills to take effect.'
)
Expand Down
61 changes: 58 additions & 3 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,54 @@ describe('InitCommand', () => {
expect(await fs.readFile(customSkill, 'utf-8')).toBe('user skill');
});

it('should not suggest an IDE restart for CLI-only tools', async () => {
const initCommand = new InitCommand({ tools: 'claude', force: true });

await initCommand.execute(testDir);

expect(getConsoleOutput()).not.toContain('Restart your IDE');
});

it('should suggest an IDE restart for IDE-resident tools', async () => {
const initCommand = new InitCommand({ tools: 'cursor', force: true });

await initCommand.execute(testDir);

expect(getConsoleOutput()).toContain('Restart your IDE');
});

it('should suggest an IDE restart when a mix of CLI and IDE tools is configured', async () => {
// One IDE-resident tool (cursor) among CLI tools (claude) is enough: the
// hint targets the tool that needs it, so the gate must not require every
// configured tool to be IDE-resident.
const initCommand = new InitCommand({ tools: 'claude,cursor', force: true });

await initCommand.execute(testDir);

expect(getConsoleOutput()).toContain('Restart your IDE');
});

it('should word the restart hint for commands when an IDE tool gets a command surface', async () => {
// Default delivery generates commands for an adapter-backed IDE tool, so the
// hint must name commands, driven by the IDE tool's own generated surface.
const initCommand = new InitCommand({ tools: 'cursor', force: true });

await initCommand.execute(testDir);

expect(getConsoleOutput()).toContain('Restart your IDE for the new commands to take effect.');
});

it('should word the restart hint for skills when an IDE tool gets only a skill surface', async () => {
// Skills-only delivery generates no commands, so the same IDE tool must be
// told about skills, not commands.
saveGlobalConfig({ featureFlags: {}, profile: 'core', delivery: 'skills' });
const initCommand = new InitCommand({ tools: 'cursor', force: true });

await initCommand.execute(testDir);

expect(getConsoleOutput()).toContain('Restart your IDE for the new skills to take effect.');
});

it('should create skills for multiple tools at once', async () => {
const initCommand = new InitCommand({ tools: 'claude,cursor', force: true });

Expand Down Expand Up @@ -1678,10 +1726,10 @@ describe('InitCommand - profile and detection features', () => {
expect(startHint).not.toContain('/openspec-propose');
expect(startHint).not.toContain('/opsx:propose');

// No slash commands were generated, so the restart line must not claim any
// Codex is a CLI tool: its skills load as soon as the files exist, with no
// IDE process to restart, so the restart line must not appear at all (#1067).
const restartHint = logCalls.find((entry) => entry.includes('Restart your IDE'));
expect(restartHint).toContain('Restart your IDE for the new skills to take effect.');
expect(restartHint).not.toContain('slash commands');
expect(restartHint).toBeUndefined();
});

it('should print the @-prefixed prompt hint for amazon-q (prompt library, no slash surface)', async () => {
Expand Down Expand Up @@ -1956,3 +2004,10 @@ async function directoryExists(dirPath: string): Promise<boolean> {
return false;
}
}

function getConsoleOutput(): string {
return (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls
.flat()
.map(String)
.join('\n');
}
Loading