From 93ad6f5fd58b09b5693541372511a3b5d7d199a5 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 15 May 2026 16:33:40 -0700 Subject: [PATCH 1/4] fix: only show 'Restart your IDE' message for IDE-embedded tools Closes #1067 --- .changeset/restart-ide-hint.md | 7 +++++++ src/core/config.ts | 23 ++++++++++++----------- src/core/init.ts | 20 +++++++++++++++----- test/core/init.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 .changeset/restart-ide-hint.md diff --git a/.changeset/restart-ide-hint.md b/.changeset/restart-ide-hint.md new file mode 100644 index 0000000000..4db7a28666 --- /dev/null +++ b/.changeset/restart-ide-hint.md @@ -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 Windsurf was configured. diff --git a/src/core/config.ts b/src/core/config.ts index 68f1abd33c..f930fe3b62 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -16,28 +16,29 @@ export interface AIToolOption { successLabel?: string; skillsDir?: string; // e.g., '.claude' - /skills suffix per Agent Skills spec detectionPaths?: string[]; // Override skillsDir for auto-detection; any path existing triggers detection + requiresIdeRestart?: boolean; // True when slash commands are loaded by an IDE/editor process } export const AI_TOOLS: AIToolOption[] = [ - { name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' }, + { 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' }, { 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: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' }, { 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: '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 CLI', value: 'kimi', available: true, successLabel: 'Kimi CLI', skillsDir: '.kimi' }, { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' }, { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' }, @@ -45,8 +46,8 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' }, { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' }, { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' }, - { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' }, - { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' }, - { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf' }, + { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo', requiresIdeRestart: true }, + { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae', requiresIdeRestart: true }, + { name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf', skillsDir: '.windsurf', requiresIdeRestart: true }, { name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' } ]; diff --git a/src/core/init.ts b/src/core/init.ts index aa38408f22..fea61264bd 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -85,6 +85,14 @@ type InitCommandOptions = { profile?: string; }; +type SelectedTool = { + value: string; + name: string; + skillsDir: string; + wasConfigured: boolean; + requiresIdeRestart?: boolean; +}; + // ----------------------------------------------------------------------------- // Init Command Class // ----------------------------------------------------------------------------- @@ -417,8 +425,8 @@ export class InitCommand { private validateTools( toolIds: string[], toolStates: Map - ): Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> { - const validatedTools: Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> = []; + ): SelectedTool[] { + const validatedTools: SelectedTool[] = []; for (const toolId of toolIds) { const tool = AI_TOOLS.find((t) => t.value === toolId); @@ -442,6 +450,7 @@ export class InitCommand { name: tool.name, skillsDir: tool.skillsDir, wasConfigured: preState?.configured ?? false, + requiresIdeRestart: tool.requiresIdeRestart, }); } @@ -493,7 +502,7 @@ export class InitCommand { private async generateSkillsAndCommands( projectPath: string, - tools: Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }> + tools: SelectedTool[] ): Promise<{ createdTools: typeof tools; refreshedTools: typeof tools; @@ -716,8 +725,9 @@ 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 - if (results.createdTools.length > 0 || results.refreshedTools.length > 0) { + // Restart instruction for tools whose slash commands are loaded by an IDE/editor + const configuredTools = [...results.createdTools, ...results.refreshedTools]; + if (configuredTools.some((tool) => tool.requiresIdeRestart)) { console.log(); console.log(chalk.white('Restart your IDE for slash commands to take effect.')); } diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 6a436eaed1..4c9be42498 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -192,6 +192,22 @@ describe('InitCommand', () => { ).toBe(true); }); + 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 for slash commands to take effect.'); + }); + + 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 for slash commands to take effect.'); + }); + it('should create skills for multiple tools at once', async () => { const initCommand = new InitCommand({ tools: 'claude,cursor', force: true }); @@ -784,3 +800,10 @@ async function directoryExists(dirPath: string): Promise { return false; } } + +function getConsoleOutput(): string { + return (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls + .flat() + .map(String) + .join('\n'); +} From 4c7c8ac564cf5a236542af5bc4dc7cc9de7b5c86 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 15 May 2026 17:34:57 -0700 Subject: [PATCH 2/4] address CodeRabbit review: type tools parameter as SelectedTool[] displaySuccessMessage accessed tool.requiresIdeRestart on objects derived from results.createdTools / results.refreshedTools, but the parameter type was an inline anonymous shape that didn't include the field. Use the existing SelectedTool type so the access is type-safe. --- src/core/init.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/init.ts b/src/core/init.ts index fea61264bd..5ec2895c92 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -634,10 +634,10 @@ export class InitCommand { private displaySuccessMessage( projectPath: string, - tools: Array<{ value: string; name: string; skillsDir: string; wasConfigured: boolean }>, + tools: SelectedTool[], results: { - createdTools: typeof tools; - refreshedTools: typeof tools; + createdTools: SelectedTool[]; + refreshedTools: SelectedTool[]; failedTools: Array<{ name: string; error: Error }>; commandsSkipped: string[]; removedCommandCount: number; From 6ec2e069e867d4212809c962ec4a1dab5751976f Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 7 Aug 2026 16:26:41 -0500 Subject: [PATCH 3/4] harden: couple restart hint to the same tool's artifacts; classify IDE tools Address CodeRabbit's Major finding on the restart-hint gate: the previous condition checked "some tool is IDE-resident" AND "some tool generated a surface" as independent aggregates, so a CLI tool's output could trigger the hint for an unrelated IDE tool, and the message could say "commands" when the IDE tool only generated skills. Compute restartCommandsGenerated / restartSkillsGenerated per-tool, coupling "requiresIdeRestart" and the shouldGenerate* result to the SAME tool; drive both the gate and the wording from them. Also flag four high-confidence standalone-IDE tools that main added after #1097 was written (Antigravity, Kiro, Lingma, Qoder) so genuine IDEs keep their restart hint instead of silently losing it under the unflagged default. The two genuinely dual-surface tools (CodeArts, ZCode) are left unflagged: under-flagging only costs a missing hint, over-flagging reintroduces #1067. Add a mixed CLI+IDE regression test. Co-Authored-By: Claude Opus 4.8 --- src/core/config.ts | 8 ++++---- src/core/init.ts | 34 ++++++++++++++++++++++++---------- test/core/init.test.ts | 11 +++++++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/core/config.ts b/src/core/config.ts index 5065351dec..ee987b5e79 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -39,7 +39,7 @@ export interface AIToolOption { export const AI_TOOLS: AIToolOption[] = [ { 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' }, + { 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' }, @@ -61,14 +61,14 @@ export const AI_TOOLS: AIToolOption[] = [ { 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', requiresIdeRestart: true }, diff --git a/src/core/init.ts b/src/core/init.ts index 3c53d0abf9..e62bb1966c 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -1276,19 +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 only for tools whose commands/skills are loaded by an - // IDE/editor process (CLI tools pick up the generated files immediately, so a - // restart line would be wrong for them — see #1067), and only when a surface - // was actually generated (nothing generated means nothing a restart picks 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. - const anyRequiresIdeRestart = successfulTools.some((tool) => tool.requiresIdeRestart); - if (anyRequiresIdeRestart && (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.' ) diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 7996d145e1..63ff369976 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -732,6 +732,17 @@ describe('InitCommand', () => { 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 create skills for multiple tools at once', async () => { const initCommand = new InitCommand({ tools: 'claude,cursor', force: true }); From af809034d50a6856b166a5e525f9845a1638ec3f Mon Sep 17 00:00:00 2001 From: Clay Good Date: Fri, 7 Aug 2026 16:46:26 -0500 Subject: [PATCH 4/4] harden: document dual-surface flag limitation; pin restart-hint wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second review pass follow-ups: - Expand the requiresIdeRestart doc comment to record that a per-tool boolean cannot distinguish the IDE vs CLI variant of tools that ship both (Continue, Amazon Q); they are flagged as IDE because that is the common case. - Add two tests pinning the hint wording branches: an IDE tool with a command surface says "…new commands…"; the same tool under skills-only delivery says "…new skills…". Previously only the presence/absence of the line was covered. Co-Authored-By: Claude Opus 4.8 --- src/core/config.ts | 8 +++++++- test/core/init.test.ts | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/core/config.ts b/src/core/config.ts index ee987b5e79..202d441fd4 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -34,7 +34,13 @@ 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 - requiresIdeRestart?: boolean; // True when slash commands are loaded by an IDE/editor process + // 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[] = [ diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 63ff369976..59de7a5308 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -743,6 +743,27 @@ describe('InitCommand', () => { 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 });