From fc2a7c05550751ec7565513c3e5cd5ba6195abc7 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 23 Jun 2026 12:10:21 -0400 Subject: [PATCH] fix(#1615): install Windsurf slash workflows --- .changeset/jolly-hawks-run.md | 5 + bin/install.js | 53 +++++++-- capabilities/windsurf/capability.json | 19 +-- docs/ARCHITECTURE.md | 4 +- docs/how-to/install-on-your-runtime.md | 2 +- docs/installer-migrations.md | 2 +- docs/reference/skill-mapping-matrix.md | 2 +- gsd-core/bin/lib/capability-registry.cjs | 38 ++---- gsd-core/bin/lib/capability-validator.cjs | 1 + src/runtime-artifact-conversion.cts | 36 ++++-- src/runtime-name-policy.cts | 4 +- src/update-context.cts | 4 +- tests/helpers/install-shared.cjs | 6 +- tests/install-runtime-artifacts.test.cjs | 43 ++++++- tests/install.test.cjs | 109 +++++++----------- ...-artifact-layout-descriptor-drive.test.cjs | 7 +- tests/runtime-artifact-layout.test.cjs | 15 ++- tests/windsurf-conversion.test.cjs | 34 +++++- 18 files changed, 222 insertions(+), 162 deletions(-) create mode 100644 .changeset/jolly-hawks-run.md diff --git a/.changeset/jolly-hawks-run.md b/.changeset/jolly-hawks-run.md new file mode 100644 index 000000000..b466844e8 --- /dev/null +++ b/.changeset/jolly-hawks-run.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 0 +--- +**Windsurf installs expose /gsd-* commands in Cascade again** — Windsurf runtime installs now emit workflow files under .windsurf/workflows instead of dead skills-only artifacts. (#1615) diff --git a/bin/install.js b/bin/install.js index 456d9ac25..b054b5091 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2457,20 +2457,18 @@ function convertClaudeToWindsurfMarkdown(content) { // Replace subagent_type from Claude to Windsurf format converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); - // Replace project-level Claude conventions with Windsurf/Devin equivalents - // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). - // Legacy .windsurf/ is still recognized on read but new installs use .devin/. - converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`'); - converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules'); - converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`'); - converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules'); - converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/'); - converted = converted.replace(/\.\/\.claude\//g, './.devin/'); - converted = converted.replace(/\.claude\//g, '.devin/'); + // Replace project-level Claude conventions with Windsurf equivalents. + converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`'); + converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules'); + converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`'); + converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules'); + converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/'); + converted = converted.replace(/\.\/\.claude\//g, './.windsurf/'); + converted = converted.replace(/\.claude\//g, '.windsurf/'); // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite. // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore. - converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin'); - converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin'); + converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf'); + converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf'); // Environment variable name rewrite converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR'); // Remove Claude Code-specific bug workarounds before brand replacement @@ -2524,6 +2522,19 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) { return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } +function convertClaudeCommandToWindsurfWorkflow(content, commandName) { + const converted = convertClaudeToWindsurfMarkdown(content); + const { frontmatter } = extractFrontmatterAndBody(converted); + const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : ''; + const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName; + const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`; + const byteLength = Buffer.byteLength(workflow, 'utf8'); + if (byteLength > 12000) { + throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`); + } + return workflow; +} + /** * Convert Claude Code agent markdown to Windsurf agent format. * Strips frontmatter fields Windsurf doesn't support (color, skills), @@ -9638,6 +9649,23 @@ function install(isGlobal, runtime = 'claude', options = {}) { } else { failures.push('agents/gsd.yaml'); } + } else if (isWindsurf) { + if (isGlobal) { + console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`); + } else { + const workflowsDir = path.join(targetDir, 'workflows'); + if (fs.existsSync(workflowsDir)) { + const workflowCount = fs.readdirSync(workflowsDir) + .filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length; + if (workflowCount > 0) { + console.log(` ${green}✓${reset} Installed ${workflowCount} workflows to workflows/`); + } else { + failures.push('workflows/gsd-*'); + } + } else { + failures.push('workflows/gsd-*'); + } + } } else { const skillsDir = path.join(targetDir, 'skills'); if (fs.existsSync(skillsDir)) { @@ -11980,6 +12008,7 @@ module.exports = { skillFrontmatterName, convertClaudeToWindsurfMarkdown, convertClaudeCommandToWindsurfSkill, + convertClaudeCommandToWindsurfWorkflow, convertClaudeAgentToWindsurfAgent, convertClaudeToAugmentMarkdown, convertClaudeCommandToAugmentSkill, diff --git a/capabilities/windsurf/capability.json b/capabilities/windsurf/capability.json index 5924ff729..92e43e223 100644 --- a/capabilities/windsurf/capability.json +++ b/capabilities/windsurf/capability.json @@ -3,7 +3,7 @@ "role": "runtime", "version": "1.6.0-rc.2", "title": "Windsurf", - "description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.", + "description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.", "tier": "core", "requires": [], "engines": { @@ -20,24 +20,15 @@ }, "configFormat": "none", "artifactLayout": { - "global": [ - { - "kind": "skills", - "destSubpath": "skills", - "prefix": "gsd-", - "nesting": "flat", - "recursive": false, - "converter": "convertClaudeCommandToWindsurfSkill" - } - ], + "global": [], "local": [ { - "kind": "skills", - "destSubpath": "skills", + "kind": "commands", + "destSubpath": "workflows", "prefix": "gsd-", "nesting": "flat", "recursive": false, - "converter": "convertClaudeCommandToWindsurfSkill" + "converter": "convertClaudeCommandToWindsurfWorkflow" } ] }, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c8cfa0a4e..9ca0a913c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -598,7 +598,7 @@ Equivalent paths for other runtimes: - **Copilot:** `~/.copilot/` global or `./.github/` local - **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local - **Cursor:** `~/.cursor/` global or `./.cursor/` local -- **Windsurf/Devin Desktop:** `~/.codeium/windsurf/` global or `./.devin/` local (canonical, #1085); `./.windsurf/` local is still recognized as legacy +- **Windsurf/Devin Desktop:** `~/.codeium/windsurf/` global config or `./.windsurf/` local workflows - **Augment Code:** `~/.augment/` global or `./.augment/` local - **Trae:** `~/.trae/` global or `./.trae/` local - **Qwen Code:** `~/.qwen/` global or `./.qwen/` local @@ -842,7 +842,7 @@ The migration-specific ownership and source snapshots live in | GitHub Copilot | `~/.copilot` | `./.github` | `skills/gsd-*/SKILL.md` (flat), `copilot-instructions.md`, and `AGENTS.md` (repo root, local) | `.agent.md` files | Self-contained `sessionStart` hook (`hooks/gsd-session.json`, inline `command` type); no statusline | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills//SKILL.md` (nested concretes) | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | | Cursor | `~/.cursor` | `./.cursor` | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; `hooks.json` with sessionStart context injection and postToolUse STATE.md monitor (#777) | -| Windsurf | `~/.codeium/windsurf` | `./.devin` (canonical, #1085); `./.windsurf` legacy recognized | `skills/gsd-*/SKILL.md` (flat) | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks | +| Windsurf | `~/.codeium/windsurf` config | `./.windsurf` | `workflows/gsd-*.md` slash-command workflows | No custom-agent artifact surface | No GSD hooks | | Augment Code | `~/.augment` | `./.augment` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills//SKILL.md` (nested concretes) | `agents/gsd-*.md` | No GSD hooks or statusline | | Trae | `~/.trae` | `./.trae` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills//SKILL.md` (nested concretes) | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks | | Qwen Code | `~/.qwen` | `./.qwen` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills//SKILL.md` (nested concretes) | `agents/gsd-*.md` | Common GSD settings and hook entries where supported | diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index 18f6c4c6a..ddacd3353 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -318,7 +318,7 @@ npx @opengsd/gsd-core@latest --windsurf --global npx @opengsd/gsd-core@latest --devin-desktop --global ``` -Global skills land in `~/.codeium/windsurf/` (unchanged). Local workspace installs write to `.devin/skills/` (Devin Desktop's preferred location, #1085); the legacy `.windsurf/skills/` layout is still recognized for backward-compat. GSD installs skills, agents, and workspace rules. +Use a workspace install for Windsurf slash commands. Workspace installs write `/gsd-*` commands as Windsurf workflow files under `.windsurf/workflows/`. Windsurf discovers those `.md` workflow files in Cascade and exposes them through the `/` menu. Global-scope Windsurf workflow installation is intentionally a no-op for now because global workflow locations are outside GSD's normal user-owned runtime config directory. **Override the install directory:** diff --git a/docs/installer-migrations.md b/docs/installer-migrations.md index 89a084e3f..cc4ceb8fd 100644 --- a/docs/installer-migrations.md +++ b/docs/installer-migrations.md @@ -373,7 +373,7 @@ for the new shape before changing migration behavior. | GitHub Copilot | Skills in `skills/gsd-*/SKILL.md`; agents as `.agent.md`; repository instructions in `copilot-instructions.md` | Global `COPILOT_CONFIG_DIR`, `COPILOT_HOME`, or `~/.copilot`; local `./.github` | GSD owns generated skill/agent files and GSD-authored instruction files; no hook/statusline ownership | [Repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions), [Copilot CLI custom instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/add-custom-instructions); GitHub Docs product docs, checked 2026-05-11 | | Antigravity | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; Gemini-style `settings.json` hooks when installed by GSD | Global `ANTIGRAVITY_CONFIG_DIR` or `~/.gemini/antigravity`; local `./.agents` (canonical, #791) or `./.agent` (legacy, recognized for backward-compat) | GSD owns generated skills/agents/hooks and GSD settings entries only | Public Antigravity install/config docs for this file layout were not stable or complete as of 2026-05-11; installer compatibility therefore uses GSD's Gemini-compatible settings policy, documented shim baseline. Fresh installs write to `.agents/` (the Google-Codelabs-documented form); existing `.agent/` installs continue to be detected and served. | | Cursor | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/`; lifecycle hooks via `hooks.json` (sessionStart + postToolUse, #777) | Global `CURSOR_CONFIG_DIR` or `~/.cursor`; local `./.cursor` | GSD owns generated skills/agents, GSD rule files or references, and GSD-managed `hooks.json` entries (sentinel `gsd-managed:true`); no statusline ownership | [Cursor rules](https://docs.cursor.com/context/rules); [Cursor hooks](https://docs.cursor.com/context/hooks); docs not versioned, checked 2026-06-07 | -| Windsurf / Devin Desktop | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `WINDSURF_CONFIG_DIR` or `~/.codeium/windsurf`; local `./.devin` (canonical, #1085) or `./.windsurf` (legacy, recognized for backward-compat) | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Windsurf has rebranded to Devin Desktop; workspace skills install to `.devin/` per Devin Desktop documented preferred location (#1085). Global `~/.codeium/windsurf/` is unchanged. Windsurf public rule docs were source-limited in search results as of 2026-05-11; installer targets the common workspace rules convention `./.devin/rules` and must be rechecked before migrations rewrite rules | +| Windsurf / Devin Desktop | Local slash-command workflows in `workflows/gsd-*.md`; no custom-agent artifact surface | Local workflow directory `./.windsurf/workflows`; global workflow install is intentionally a no-op | GSD owns generated local workflow files only; no hook/statusline ownership | Windsurf workflows are the documented `/` command surface. Workspace workflows live under `.windsurf/workflows/*.md`; global workflow locations are outside GSD's normal user-owned runtime config directory and are not written by the GSD installer. | | Augment Code | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/` | Global `AUGMENT_CONFIG_DIR` or `~/.augment`; local `./.augment` | GSD owns generated skills/agents only; no hook/statusline ownership | [Augment Agent Skills](https://docs.augmentcode.com/cli/skills), [Augment IDE skills](https://docs.augmentcode.com/using-augment/skills); IDE skills public beta in VS Code 0.789.0+, checked 2026-05-11 | | Trae | Skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; rule references under `rules/` | Global `TRAE_CONFIG_DIR` or `~/.trae`; local `./.trae` | GSD owns generated skills/agents and GSD rule files or references; no hook/statusline ownership | Public Trae docs expose AI settings and `.rules` announcements, but no stable skills/config API was found as of 2026-05-11; migrations must treat this row as source-limited | | Qwen Code | Claude-compatible skills in `skills/gsd-*/SKILL.md`; agents in `agents/`; optional common hook/settings integration through GSD | Global `QWEN_CONFIG_DIR` or `~/.qwen`; local `./.qwen` | GSD owns generated skills/agents/hooks and GSD settings entries only | [Qwen commands and skills](https://qwenlm.github.io/qwen-code-docs/en/users/features/commands/); docs last updated 2026-05-06 | diff --git a/docs/reference/skill-mapping-matrix.md b/docs/reference/skill-mapping-matrix.md index af3e6a699..1a683f12a 100644 --- a/docs/reference/skill-mapping-matrix.md +++ b/docs/reference/skill-mapping-matrix.md @@ -32,7 +32,7 @@ For the transform each converter applies, see [ADR-1593 §3 — converter transf | **cursor** | `skills/` | `gsd-` | flat | recursive | `convertClaudeCommandToCursorSkill` | Also ships flat `commands/` via `convertClaudeCommandToCursorCommand`. `configFormat: none`. | | **copilot** | `skills/` | `gsd-` | flat | unconfirmed → conservative | `convertClaudeCommandToCopilotSkill` | Markdown config. Scope-aware converter (global-home vs workspace-relative). | | **antigravity** | `skills/` | `gsd-` | nested | non-recursive (one-level) | `convertClaudeCommandToAntigravitySkill` | `dot-home-nested` config home. Scope-aware converter. Nesting confirmed: *"will not recursive scan"*. | -| **windsurf** | `skills/` | `gsd-` | flat | unconfirmed → conservative | `convertClaudeCommandToWindsurfSkill` | `configFormat: none`. `installSurface: profile-marker-only`. | +| **windsurf** | — *(no skills kind)* | `gsd-` | — | workflows | `convertClaudeCommandToWindsurfWorkflow` | Emits `.windsurf/workflows/gsd-*.md` slash-command workflows. `configFormat: none`. `installSurface: profile-marker-only`. | | **augment** | `skills/` | `gsd-` | nested | non-recursive (single-level) | `convertClaudeCommandToAugmentSkill` | Also ships flat `commands/`. Settings-json config. | | **trae** | `skills/` | `gsd-` | nested | non-recursive (flat; nesting errors) | `convertClaudeCommandToTraeSkill` | `configFormat: none`. Trae IDE (trae.ai), not trae-agent. | | **qwen** | `skills/` | `gsd-` | nested | non-recursive (flat readdir) | `convertClaudeCommandToClaudeSkill` | **Shares Claude's converter.** Emits numeric `priority:` (`QWEN_SKILL_PRIORITY`) for `/skills` ordering. Settings-json config. | diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index c26051ea1..62758463f 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -1977,7 +1977,7 @@ const capabilities = { "role": "runtime", "version": "1.6.0-rc.2", "title": "Windsurf", - "description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.", + "description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.", "tier": "core", "requires": [], "engines": { @@ -1994,24 +1994,15 @@ const capabilities = { }, "configFormat": "none", "artifactLayout": { - "global": [ - { - "kind": "skills", - "destSubpath": "skills", - "prefix": "gsd-", - "nesting": "flat", - "recursive": false, - "converter": "convertClaudeCommandToWindsurfSkill" - } - ], + "global": [], "local": [ { - "kind": "skills", - "destSubpath": "skills", + "kind": "commands", + "destSubpath": "workflows", "prefix": "gsd-", "nesting": "flat", "recursive": false, - "converter": "convertClaudeCommandToWindsurfSkill" + "converter": "convertClaudeCommandToWindsurfWorkflow" } ] }, @@ -3656,7 +3647,7 @@ const runtimes = { "role": "runtime", "version": "1.6.0-rc.2", "title": "Windsurf", - "description": "Windsurf (Codeium) — nested under ~/.codeium/windsurf; skills-only artifact layout; no hook surface; no hook events; tier-2 support.", + "description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.", "tier": "core", "requires": [], "engines": { @@ -3673,24 +3664,15 @@ const runtimes = { }, "configFormat": "none", "artifactLayout": { - "global": [ - { - "kind": "skills", - "destSubpath": "skills", - "prefix": "gsd-", - "nesting": "flat", - "recursive": false, - "converter": "convertClaudeCommandToWindsurfSkill" - } - ], + "global": [], "local": [ { - "kind": "skills", - "destSubpath": "skills", + "kind": "commands", + "destSubpath": "workflows", "prefix": "gsd-", "nesting": "flat", "recursive": false, - "converter": "convertClaudeCommandToWindsurfSkill" + "converter": "convertClaudeCommandToWindsurfWorkflow" } ] }, diff --git a/gsd-core/bin/lib/capability-validator.cjs b/gsd-core/bin/lib/capability-validator.cjs index 8d301a7f8..1f1a08b2f 100644 --- a/gsd-core/bin/lib/capability-validator.cjs +++ b/gsd-core/bin/lib/capability-validator.cjs @@ -665,6 +665,7 @@ const VALID_CONVERTER_NAMES = new Set([ 'convertClaudeCommandToOpencodeSkill', 'convertClaudeCommandToTraeSkill', 'convertClaudeCommandToWindsurfSkill', + 'convertClaudeCommandToWindsurfWorkflow', // agent converters (#1173 — descriptor-driven agent conversion wiring) 'convertClaudeAgentToCopilotAgent', 'convertClaudeAgentToAntigravityAgent', diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index de0fd0095..7f8f48d10 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -941,20 +941,18 @@ function convertClaudeToWindsurfMarkdown(content) { // Replace subagent_type from Claude to Windsurf format converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); - // Replace project-level Claude conventions with Windsurf/Devin equivalents - // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). - // Legacy .windsurf/ is still recognized on read but new installs use .devin/. - converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`'); - converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules'); - converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`'); - converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules'); - converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/'); - converted = converted.replace(/\.\/\.claude\//g, './.devin/'); - converted = converted.replace(/\.claude\//g, '.devin/'); + // Replace project-level Claude conventions with Windsurf equivalents. + converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`'); + converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules'); + converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`'); + converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules'); + converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/'); + converted = converted.replace(/\.\/\.claude\//g, './.windsurf/'); + converted = converted.replace(/\.claude\//g, '.windsurf/'); // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite. // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore. - converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin'); - converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin'); + converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf'); + converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf'); // Environment variable name rewrite converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR'); // Remove Claude Code-specific bug workarounds before brand replacement @@ -1008,6 +1006,19 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) { return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } +function convertClaudeCommandToWindsurfWorkflow(content, commandName) { + const converted = convertClaudeToWindsurfMarkdown(content); + const { frontmatter } = extractFrontmatterAndBody(converted); + const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : ''; + const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName; + const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`; + const byteLength = Buffer.byteLength(workflow, 'utf8'); + if (byteLength > 12000) { + throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`); + } + return workflow; +} + // --- Augment converters --- // Augment uses a tool set similar to Cursor/Windsurf. // Config lives in .augment/ (local) and ~/.augment/ (global). @@ -2568,6 +2579,7 @@ export = { convertClaudeCommandToCursorCommand, convertClaudeToWindsurfMarkdown, convertClaudeCommandToWindsurfSkill, + convertClaudeCommandToWindsurfWorkflow, convertClaudeToAugmentMarkdown, convertClaudeCommandToAugmentSkill, convertClaudeToTraeMarkdown, diff --git a/src/runtime-name-policy.cts b/src/runtime-name-policy.cts index 84d45233b..0204106d0 100644 --- a/src/runtime-name-policy.cts +++ b/src/runtime-name-policy.cts @@ -133,7 +133,7 @@ export function getProjectInstructionFile(runtime: unknown): string { /** * Map a canonical runtime id to its on-disk local config directory name - * (e.g. `cursor` -> `.cursor`, `windsurf` -> `.devin`). Unknown/empty inputs + * (e.g. `cursor` -> `.cursor`, `windsurf` -> `.windsurf`). Unknown/empty inputs * fall back to `.claude`. * * Pure runtime-identity projection. Relocated from `bin/install.js` per @@ -149,7 +149,7 @@ export function getDirName(runtime: string): string { if (runtime === 'codex') return '.codex'; if (runtime === 'antigravity') return '.agents'; if (runtime === 'cursor') return '.cursor'; - if (runtime === 'windsurf') return '.devin'; + if (runtime === 'windsurf') return '.windsurf'; if (runtime === 'augment') return '.augment'; if (runtime === 'trae') return '.trae'; if (runtime === 'qwen') return '.qwen'; diff --git a/src/update-context.cts b/src/update-context.cts index 16bbb2f33..9492f22bb 100644 --- a/src/update-context.cts +++ b/src/update-context.cts @@ -26,8 +26,8 @@ export const RUNTIME_DIRS: RuntimeDirEntry[] = [ ['antigravity', '.gemini/antigravity'], ['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity')) ['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs) - ['windsurf', '.devin'], // local Windsurf/Devin Desktop install dir canonical (#1085; bin/install.js getDirName('windsurf')) - ['windsurf', '.windsurf'], // local Windsurf install dir legacy (#1085; backward-compat with pre-#1085 installs) + ['windsurf', '.windsurf'], // local Windsurf workflow dir canonical (#1615; bin/install.js getDirName('windsurf')) + ['windsurf', '.devin'], // local Devin Desktop install dir legacy (#1085; backward-compat) ['gemini', '.gemini'], ['kilo', '.config/kilo'], ['kilo', '.kilo'], diff --git a/tests/helpers/install-shared.cjs b/tests/helpers/install-shared.cjs index 8d111101d..3ede59d96 100644 --- a/tests/helpers/install-shared.cjs +++ b/tests/helpers/install-shared.cjs @@ -56,13 +56,13 @@ const RUNTIME_META = { opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, trae: { localDir: '.trae', globalSuffix: '.trae' }, - windsurf: { localDir: '.devin', globalSuffix: path.join('.codeium', 'windsurf') }, + windsurf: { localDir: '.windsurf', globalSuffix: path.join('.codeium', 'windsurf') }, }; // Runtimes that emit per-skill files under skills/ (not rules-based or commands-based) const SKILL_RUNTIMES = [ 'claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity', - 'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy', + 'cursor', 'augment', 'trae', 'qwen', 'codebuddy', ]; // ─── Helper functions ───────────────────────────────────────────────────────── @@ -114,7 +114,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [] }) { const LOCAL_DIR_NAME = { claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo', codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor', - windsurf: '.devin', augment: '.augment', trae: '.trae', qwen: '.qwen', + windsurf: '.windsurf', augment: '.augment', trae: '.trae', qwen: '.qwen', codebuddy: '.codebuddy', cline: '.', }; let configDir; diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index 0377c4a60..863305e57 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -140,7 +140,7 @@ describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Mod const SKILLS_RUNTIMES_LAYOUT = [ 'claude', 'cursor', 'codex', 'copilot', 'antigravity', - 'windsurf', 'augment', 'trae', 'qwen', 'kimi', 'codebuddy', + 'augment', 'trae', 'qwen', 'kimi', 'codebuddy', ]; const ALL_RUNTIMES_LAYOUT = [ @@ -298,6 +298,47 @@ describe('installRuntimeArtifacts — cursor commands layout (#785)', () => { }); }); +describe('installRuntimeArtifacts — windsurf workflows layout (#1615)', () => { + test('windsurf: local install writes workflow slash-command files, not skills', (t) => { + const configDir = createTempDir('gsd-ial-windsurf-'); + t.after(() => cleanup(configDir)); + + installRuntimeArtifacts('windsurf', configDir, 'local', RESOLVED_CORE); + + const workflowsDir = path.join(configDir, 'workflows'); + assert.ok(fs.existsSync(workflowsDir), 'workflows/ must exist for Windsurf local install'); + assert.ok(fs.existsSync(path.join(workflowsDir, 'gsd-help.md')), + 'workflows/gsd-help.md must exist for /gsd-help'); + assert.ok(!fs.existsSync(path.join(configDir, 'skills')), + 'Windsurf must not install dead skills/ artifacts for slash commands'); + + const helpContent = fs.readFileSync(path.join(workflowsDir, 'gsd-help.md'), 'utf8'); + assert.ok(!helpContent.startsWith('---'), 'Windsurf workflows must be plain markdown, not SKILL.md frontmatter'); + assert.match(helpContent, /# gsd-help/, 'workflow should identify the slash command it backs'); + assert.ok(helpContent.includes(`${configDir}/gsd-core/commands/gsd/help.md`), + 'workflow should reference the installed command body using the actual install target'); + + for (const fileName of fs.readdirSync(workflowsDir)) { + if (!fileName.endsWith('.md')) continue; + const workflowPath = path.join(workflowsDir, fileName); + const byteLength = Buffer.byteLength(fs.readFileSync(workflowPath, 'utf8'), 'utf8'); + assert.ok(byteLength <= 12000, `${fileName} must respect Windsurf's 12,000-character workflow limit`); + } + }); + + test('windsurf: global install is explicit no-op for workflow artifacts', (t) => { + const configDir = createTempDir('gsd-ial-windsurf-global-'); + t.after(() => cleanup(configDir)); + + installRuntimeArtifacts('windsurf', configDir, 'global', RESOLVED_CORE); + + assert.ok(!fs.existsSync(path.join(configDir, 'workflows')), + 'global Windsurf install must not write workflows under the config root'); + assert.ok(!fs.existsSync(path.join(configDir, 'skills')), + 'global Windsurf install must not write dead skills artifacts'); + }); +}); + describe('installRuntimeArtifacts — cline skills (#782)', () => { test('cline: global install writes gsd-prefixed skill dirs under skills/', (t) => { const configDir = createTempDir('gsd-ial-cline-'); diff --git a/tests/install.test.cjs b/tests/install.test.cjs index b27f33c6e..db2cb94ee 100644 --- a/tests/install.test.cjs +++ b/tests/install.test.cjs @@ -1269,12 +1269,12 @@ describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792) assert.deepStrictEqual(selectRuntimesFromArgs(['--devin-desktop']), ['windsurf']); }); }); -// ─── Section N: Windsurf .devin canonical workspace dir (#1085) ───────────── +// ─── Section N: Windsurf workflow slash-command install (#1615) ───────────── // allow-test-rule: runtime-contract-is-the-product -// Reads deployed skill .md files whose text IS the product surface the -// Windsurf/Devin Desktop runtime loads at startup (path references, command names). +// Reads deployed workflow .md files whose text IS the product surface the +// Windsurf runtime loads at startup (path references, command names). -describe('windsurf local install writes to .devin/ canonical dir (#1085)', () => { +describe('windsurf local install writes workflow slash commands (#1615)', () => { let tmpDir; let previousCwd; @@ -1289,50 +1289,48 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () => cleanup(tmpDir); }); - test('install writes workspace skills under .devin/skills/', () => { + test('install writes workspace workflows under .windsurf/workflows/', () => { const result = install(false, 'windsurf'); - const devinDir = path.join(tmpDir, '.devin'); + const windsurfDir = path.join(tmpDir, '.windsurf'); assert.strictEqual(result.runtime, 'windsurf'); - assert.ok(fs.existsSync(devinDir), '.devin/ must be created for local windsurf install'); - const skillsDir = path.join(devinDir, 'skills'); - assert.ok(fs.existsSync(skillsDir), '.devin/skills/ must exist after install'); - const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.ok(skillEntries.length > 0, 'at least one gsd-* skill must be installed under .devin/skills/'); - const firstSkill = path.join(skillsDir, skillEntries[0].name, 'SKILL.md'); - assert.ok(fs.existsSync(firstSkill), `SKILL.md must exist at ${firstSkill}`); + assert.ok(fs.existsSync(windsurfDir), '.windsurf/ must be created for local windsurf install'); + const workflowsDir = path.join(windsurfDir, 'workflows'); + assert.ok(fs.existsSync(workflowsDir), '.windsurf/workflows/ must exist after install'); + const workflowEntries = fs.readdirSync(workflowsDir, { withFileTypes: true }) + .filter(e => e.isFile() && e.name.startsWith('gsd-') && e.name.endsWith('.md')); + assert.ok(workflowEntries.length > 0, 'at least one gsd-* workflow must be installed under .windsurf/workflows/'); + assert.ok(fs.existsSync(path.join(workflowsDir, 'gsd-help.md')), 'gsd-help.md workflow must exist'); }); - test('legacy .windsurf/ is NOT written on a fresh local install', () => { + test('legacy .devin/skills is NOT written on a fresh local install', () => { install(false, 'windsurf'); - const legacyDir = path.join(tmpDir, '.windsurf'); - assert.ok(!fs.existsSync(legacyDir), - '.windsurf/ must not be created by a fresh install (new installs use .devin/)'); + assert.ok(!fs.existsSync(path.join(tmpDir, '.devin', 'skills')), + '.devin/skills must not be created by a fresh install (new installs use .windsurf/workflows)'); + assert.ok(!fs.existsSync(path.join(tmpDir, '.windsurf', 'skills')), + '.windsurf/skills must not be created for slash commands'); }); - test('installed skill content references .devin/ not bare .windsurf/ or ~/.claude/', () => { + test('installed workflow content references command body, not skill locations', () => { install(false, 'windsurf'); - const skillsDir = path.join(tmpDir, '.devin', 'skills'); - const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - assert.ok(skillEntries.length > 0, 'pre-condition: at least one gsd-* skill must be installed'); - for (const skillEntry of skillEntries) { - const skillFile = path.join(skillsDir, skillEntry.name, 'SKILL.md'); - if (!fs.existsSync(skillFile)) continue; - const content = fs.readFileSync(skillFile, 'utf8'); + const workflowsDir = path.join(tmpDir, '.windsurf', 'workflows'); + const workflowEntries = fs.readdirSync(workflowsDir, { withFileTypes: true }) + .filter(e => e.isFile() && e.name.startsWith('gsd-') && e.name.endsWith('.md')); + assert.ok(workflowEntries.length > 0, 'pre-condition: at least one gsd-* workflow must be installed'); + for (const workflowEntry of workflowEntries) { + const workflowFile = path.join(workflowsDir, workflowEntry.name); + const content = fs.readFileSync(workflowFile, 'utf8'); assert.ok( - !content.includes('~/.claude/') && !content.includes('$HOME/.claude/'), - `${skillEntry.name}/SKILL.md must not contain ~/.claude/ or $HOME/.claude/ in a local install`, + content.includes(`${tmpDir}/.windsurf/gsd-core/commands/gsd/`), + `${workflowEntry.name} must reference the installed canonical command body`, ); - // Local install must use workspace-relative .devin/ form, not the legacy .windsurf/ form assert.ok( - !content.includes('~/.windsurf/') && !content.includes('.windsurf/skills/'), - `${skillEntry.name}/SKILL.md must not contain bare .windsurf/ path in a local install (use .devin/ instead)`, + !content.includes('/skills/') && !content.includes('SKILL.md'), + `${workflowEntry.name} must not reference Windsurf skill locations`, ); } }); - test('global windsurf install still writes to ~/.codeium/windsurf/ (unchanged)', () => { + test('global windsurf install does not write unsupported workflows or skills', () => { const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-')); const savedHome = process.env.HOME; const savedUserProfile = process.env.USERPROFILE; @@ -1348,12 +1346,12 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () => `global windsurf install must go to codeium/windsurf path, got: ${result.configDir}`, ); assert.ok( - fs.existsSync(path.join(result.configDir, 'skills')), - 'global windsurf install must create skills/ under ~/.codeium/windsurf', + !fs.existsSync(path.join(result.configDir, 'workflows')), + 'global windsurf install must not create unsupported workflows/ under ~/.codeium/windsurf', ); assert.ok( - !fs.existsSync(path.join(homeDir, '.devin')), - '.devin/ must NOT be created by a global install (global path is ~/.codeium/windsurf)', + !fs.existsSync(path.join(result.configDir, 'skills')), + 'global windsurf install must not create dead skills/ artifacts', ); } finally { if (savedHome === undefined) delete process.env.HOME; @@ -1366,7 +1364,7 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () => } }); - test('global windsurf install skill content references codeium path not .devin/', () => { + test('global windsurf install still installs shared workflow assets', () => { const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-c-')); const savedHome = process.env.HOME; const savedUserProfile = process.env.USERPROFILE; @@ -1376,37 +1374,10 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () => process.env.USERPROFILE = homeDir; try { const result = install(true, 'windsurf'); - const skillsDir = path.join(result.configDir, 'skills'); - if (!fs.existsSync(skillsDir)) return; // no skills emitted — skip - const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter(e => e.isDirectory() && e.name.startsWith('gsd-')); - // At least one skill body must reference the codeium/windsurf global path (#1085): - // the isGlobal-threaded rewrite converts .devin/skills/ → $HOME/.codeium/windsurf/skills/ - let foundGlobalRef = false; - for (const skillEntry of skillEntries) { - const skillFile = path.join(skillsDir, skillEntry.name, 'SKILL.md'); - if (!fs.existsSync(skillFile)) continue; - const content = fs.readFileSync(skillFile, 'utf8'); - // Global skill content must not reference local workspace-relative .devin/ paths - assert.ok( - !content.includes('.devin/skills/'), - `${skillEntry.name}/SKILL.md must not reference .devin/skills/ in global install (should use codeium path)`, - ); - assert.ok( - !content.includes('~/.claude/') && !content.includes('$HOME/.claude/'), - `${skillEntry.name}/SKILL.md must not contain ~/.claude/ or $HOME/.claude/ in global install`, - ); - if (content.includes('codeium/windsurf/skills/') || content.includes('$HOME/.codeium/windsurf/skills/')) { - foundGlobalRef = true; - } - } - // Verify the global-path rewrite actually fired on at least one skill (FIX 1 guard) - if (skillEntries.some(e => fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))) { - assert.ok( - foundGlobalRef, - 'at least one global windsurf SKILL.md must reference the codeium/windsurf/skills/ path (isGlobal rewrite must have fired)', - ); - } + assert.ok(fs.existsSync(path.join(result.configDir, 'gsd-core', 'workflows', 'update.md')), + 'global windsurf install should still copy shared gsd-core workflow assets'); + assert.ok(!fs.existsSync(path.join(result.configDir, 'workflows')), + 'global windsurf install must not write workflow files into an undocumented global path'); } finally { if (savedHome === undefined) delete process.env.HOME; else process.env.HOME = savedHome; diff --git a/tests/runtime-artifact-layout-descriptor-drive.test.cjs b/tests/runtime-artifact-layout-descriptor-drive.test.cjs index 0f673d21e..a5f086dd3 100644 --- a/tests/runtime-artifact-layout-descriptor-drive.test.cjs +++ b/tests/runtime-artifact-layout-descriptor-drive.test.cjs @@ -104,12 +104,9 @@ const GOLDEN = { ], // ── windsurf ───────────────────────────────────────────────────────────────── - // Old switch: no scope branch → local == global. 5b backfill restores this. - 'windsurf/global': [ - { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, - ], + 'windsurf/global': [], 'windsurf/local': [ - { kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, + { kind: 'commands', destSubpath: 'workflows', prefix: 'gsd-' }, ], // ── augment ────────────────────────────────────────────────────────────────── diff --git a/tests/runtime-artifact-layout.test.cjs b/tests/runtime-artifact-layout.test.cjs index a52aa6768..ca6580aa6 100644 --- a/tests/runtime-artifact-layout.test.cjs +++ b/tests/runtime-artifact-layout.test.cjs @@ -134,16 +134,23 @@ describe('resolveRuntimeArtifactLayout — antigravity', () => { }); describe('resolveRuntimeArtifactLayout — windsurf', () => { - test('returns correct layout for windsurf', () => { - const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR); + test('returns local workflow layout for windsurf', () => { + const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR, 'local'); assert.strictEqual(layout.runtime, 'windsurf'); assert.strictEqual(layout.configDir, FAKE_DIR); assert.strictEqual(layout.kinds.length, 1); - assert.strictEqual(layout.kinds[0].kind, 'skills'); - assert.strictEqual(layout.kinds[0].destSubpath, 'skills'); + assert.strictEqual(layout.kinds[0].kind, 'commands'); + assert.strictEqual(layout.kinds[0].destSubpath, 'workflows'); assert.strictEqual(layout.kinds[0].prefix, 'gsd-'); assert.strictEqual(typeof layout.kinds[0].stage, 'function'); }); + + test('returns empty global layout for windsurf', () => { + const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR, 'global'); + assert.strictEqual(layout.runtime, 'windsurf'); + assert.strictEqual(layout.configDir, FAKE_DIR); + assert.strictEqual(layout.kinds.length, 0); + }); }); describe('resolveRuntimeArtifactLayout — augment', () => { diff --git a/tests/windsurf-conversion.test.cjs b/tests/windsurf-conversion.test.cjs index 34e1b4863..aa63681c7 100644 --- a/tests/windsurf-conversion.test.cjs +++ b/tests/windsurf-conversion.test.cjs @@ -13,6 +13,7 @@ const assert = require('node:assert/strict'); const { convertClaudeCommandToWindsurfSkill, + convertClaudeCommandToWindsurfWorkflow, convertClaudeAgentToWindsurfAgent, convertClaudeToWindsurfMarkdown, } = require('../bin/install.js'); @@ -73,6 +74,29 @@ Body content. }); }); +describe('convertClaudeCommandToWindsurfWorkflow', () => { + test('writes a plain workflow wrapper for slash commands', () => { + const input = `--- +name: quick +description: Execute a quick task +--- + + +Test body + +`; + + const result = convertClaudeCommandToWindsurfWorkflow(input, 'gsd-quick'); + + assert.ok(!result.startsWith('---'), 'workflow has no YAML frontmatter'); + assert.match(result, /^# gsd-quick$/m, 'workflow title names the slash command'); + assert.ok(result.includes('Execute a quick task'), 'description is preserved'); + assert.ok(result.includes('@~/.claude/gsd-core/commands/gsd/quick.md'), 'workflow delegates to canonical command body'); + assert.ok(result.includes('/gsd-quick'), 'workflow mentions the slash command invocation'); + assert.ok(Buffer.byteLength(result, 'utf8') <= 12000, 'workflow respects Windsurf limit'); + }); +}); + describe('convertClaudeAgentToWindsurfAgent', () => { test('converts agent frontmatter with unquoted name', () => { const input = `--- @@ -105,17 +129,17 @@ describe('convertClaudeToWindsurfMarkdown', () => { assert.ok(!result.includes('Claude Code'), 'original brand removed'); }); - test('replaces CLAUDE.md with .devin/rules (no trailing slash)', () => { + test('replaces CLAUDE.md with .windsurf/rules (no trailing slash)', () => { const input = 'See `CLAUDE.md` for configuration. Also check ./CLAUDE.md file.'; const result = convertClaudeToWindsurfMarkdown(input); - assert.ok(result.includes('.devin/rules'), 'CLAUDE.md replaced with .devin/rules (#1085)'); - assert.ok(!result.includes('.devin/rules/'), 'no trailing slash (Node v25 compat)'); + assert.ok(result.includes('.windsurf/rules'), 'CLAUDE.md replaced with .windsurf/rules'); + assert.ok(!result.includes('.windsurf/rules/'), 'no trailing slash (Node v25 compat)'); }); - test('replaces .claude/skills/ with .devin/skills/', () => { + test('replaces .claude/skills/ with .windsurf/skills/', () => { const input = 'Skills are stored in .claude/skills/ directory.'; const result = convertClaudeToWindsurfMarkdown(input); - assert.ok(result.includes('.devin/skills/'), 'skills path replaced with .devin/skills/ (#1085)'); + assert.ok(result.includes('.windsurf/skills/'), 'skills path replaced with .windsurf/skills/'); }); test('replaces Bash( with Shell( and Edit( with StrReplace(', () => {