fix(#1615): install Windsurf slash workflows

This commit is contained in:
Tom Boucher
2026-06-23 12:10:21 -04:00
parent c28cccbf85
commit fc2a7c0555
18 changed files with 222 additions and 162 deletions

View File

@@ -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)

View File

@@ -2457,20 +2457,18 @@ function convertClaudeToWindsurfMarkdown(content) {
// Replace subagent_type from Claude to Windsurf format // Replace subagent_type from Claude to Windsurf format
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
// Replace project-level Claude conventions with Windsurf/Devin equivalents // Replace project-level Claude conventions with Windsurf equivalents.
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
// Legacy .windsurf/ is still recognized on read but new installs use .devin/. converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`'); converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`'); converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules'); converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/'); converted = converted.replace(/\.claude\//g, '.windsurf/');
converted = converted.replace(/\.\/\.claude\//g, './.devin/');
converted = converted.replace(/\.claude\//g, '.devin/');
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite. // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore. // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin'); converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin'); converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
// Environment variable name rewrite // Environment variable name rewrite
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR'); converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
// Remove Claude Code-specific bug workarounds before brand replacement // 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()}`; 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. * Convert Claude Code agent markdown to Windsurf agent format.
* Strips frontmatter fields Windsurf doesn't support (color, skills), * Strips frontmatter fields Windsurf doesn't support (color, skills),
@@ -9638,6 +9649,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
} else { } else {
failures.push('agents/gsd.yaml'); 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 { } else {
const skillsDir = path.join(targetDir, 'skills'); const skillsDir = path.join(targetDir, 'skills');
if (fs.existsSync(skillsDir)) { if (fs.existsSync(skillsDir)) {
@@ -11980,6 +12008,7 @@ module.exports = {
skillFrontmatterName, skillFrontmatterName,
convertClaudeToWindsurfMarkdown, convertClaudeToWindsurfMarkdown,
convertClaudeCommandToWindsurfSkill, convertClaudeCommandToWindsurfSkill,
convertClaudeCommandToWindsurfWorkflow,
convertClaudeAgentToWindsurfAgent, convertClaudeAgentToWindsurfAgent,
convertClaudeToAugmentMarkdown, convertClaudeToAugmentMarkdown,
convertClaudeCommandToAugmentSkill, convertClaudeCommandToAugmentSkill,

View File

@@ -3,7 +3,7 @@
"role": "runtime", "role": "runtime",
"version": "1.6.0-rc.2", "version": "1.6.0-rc.2",
"title": "Windsurf", "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", "tier": "core",
"requires": [], "requires": [],
"engines": { "engines": {
@@ -20,24 +20,15 @@
}, },
"configFormat": "none", "configFormat": "none",
"artifactLayout": { "artifactLayout": {
"global": [ "global": [],
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToWindsurfSkill"
}
],
"local": [ "local": [
{ {
"kind": "skills", "kind": "commands",
"destSubpath": "skills", "destSubpath": "workflows",
"prefix": "gsd-", "prefix": "gsd-",
"nesting": "flat", "nesting": "flat",
"recursive": false, "recursive": false,
"converter": "convertClaudeCommandToWindsurfSkill" "converter": "convertClaudeCommandToWindsurfWorkflow"
} }
] ]
}, },

View File

@@ -598,7 +598,7 @@ Equivalent paths for other runtimes:
- **Copilot:** `~/.copilot/` global or `./.github/` local - **Copilot:** `~/.copilot/` global or `./.github/` local
- **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local - **Antigravity:** auto-detected global root (`~/.gemini/antigravity/`, `~/.gemini/antigravity-ide/`, or `~/.gemini/antigravity-cli/`) or `./.agent/` local
- **Cursor:** `~/.cursor/` global or `./.cursor/` 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 - **Augment Code:** `~/.augment/` global or `./.augment/` local
- **Trae:** `~/.trae/` global or `./.trae/` local - **Trae:** `~/.trae/` global or `./.trae/` local
- **Qwen Code:** `~/.qwen/` global or `./.qwen/` 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 | | 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/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Gemini-style `settings.json` hook entries when installed by GSD | | Antigravity | auto-detected: `~/.gemini/antigravity`, `~/.gemini/antigravity-ide`, or `~/.gemini/antigravity-cli` | `./.agent` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/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) | | 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/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | No GSD hooks or statusline | | Augment Code | `~/.augment` | `./.augment` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/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/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks | | Trae | `~/.trae` | `./.trae` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/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/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Common GSD settings and hook entries where supported | | Qwen Code | `~/.qwen` | `./.qwen` | `skills/gsd-ns-*/SKILL.md` (6 routers) + `skills/gsd-ns-*/skills/<name>/SKILL.md` (nested concretes) | `agents/gsd-*.md` | Common GSD settings and hook entries where supported |

View File

@@ -318,7 +318,7 @@ npx @opengsd/gsd-core@latest --windsurf --global
npx @opengsd/gsd-core@latest --devin-desktop --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:** **Override the install directory:**

View File

@@ -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 | | 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. | | 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 | | 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 | | 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 | | 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 | | 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 |

View File

@@ -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`. | | **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). | | **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"*. | | **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. | | **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. | | **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. | | **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. |

View File

@@ -1977,7 +1977,7 @@ const capabilities = {
"role": "runtime", "role": "runtime",
"version": "1.6.0-rc.2", "version": "1.6.0-rc.2",
"title": "Windsurf", "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", "tier": "core",
"requires": [], "requires": [],
"engines": { "engines": {
@@ -1994,24 +1994,15 @@ const capabilities = {
}, },
"configFormat": "none", "configFormat": "none",
"artifactLayout": { "artifactLayout": {
"global": [ "global": [],
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToWindsurfSkill"
}
],
"local": [ "local": [
{ {
"kind": "skills", "kind": "commands",
"destSubpath": "skills", "destSubpath": "workflows",
"prefix": "gsd-", "prefix": "gsd-",
"nesting": "flat", "nesting": "flat",
"recursive": false, "recursive": false,
"converter": "convertClaudeCommandToWindsurfSkill" "converter": "convertClaudeCommandToWindsurfWorkflow"
} }
] ]
}, },
@@ -3656,7 +3647,7 @@ const runtimes = {
"role": "runtime", "role": "runtime",
"version": "1.6.0-rc.2", "version": "1.6.0-rc.2",
"title": "Windsurf", "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", "tier": "core",
"requires": [], "requires": [],
"engines": { "engines": {
@@ -3673,24 +3664,15 @@ const runtimes = {
}, },
"configFormat": "none", "configFormat": "none",
"artifactLayout": { "artifactLayout": {
"global": [ "global": [],
{
"kind": "skills",
"destSubpath": "skills",
"prefix": "gsd-",
"nesting": "flat",
"recursive": false,
"converter": "convertClaudeCommandToWindsurfSkill"
}
],
"local": [ "local": [
{ {
"kind": "skills", "kind": "commands",
"destSubpath": "skills", "destSubpath": "workflows",
"prefix": "gsd-", "prefix": "gsd-",
"nesting": "flat", "nesting": "flat",
"recursive": false, "recursive": false,
"converter": "convertClaudeCommandToWindsurfSkill" "converter": "convertClaudeCommandToWindsurfWorkflow"
} }
] ]
}, },

View File

@@ -665,6 +665,7 @@ const VALID_CONVERTER_NAMES = new Set([
'convertClaudeCommandToOpencodeSkill', 'convertClaudeCommandToOpencodeSkill',
'convertClaudeCommandToTraeSkill', 'convertClaudeCommandToTraeSkill',
'convertClaudeCommandToWindsurfSkill', 'convertClaudeCommandToWindsurfSkill',
'convertClaudeCommandToWindsurfWorkflow',
// agent converters (#1173 — descriptor-driven agent conversion wiring) // agent converters (#1173 — descriptor-driven agent conversion wiring)
'convertClaudeAgentToCopilotAgent', 'convertClaudeAgentToCopilotAgent',
'convertClaudeAgentToAntigravityAgent', 'convertClaudeAgentToAntigravityAgent',

View File

@@ -941,20 +941,18 @@ function convertClaudeToWindsurfMarkdown(content) {
// Replace subagent_type from Claude to Windsurf format // Replace subagent_type from Claude to Windsurf format
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
// Replace project-level Claude conventions with Windsurf/Devin equivalents // Replace project-level Claude conventions with Windsurf equivalents.
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
// Legacy .windsurf/ is still recognized on read but new installs use .devin/. converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`'); converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`'); converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules'); converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/'); converted = converted.replace(/\.claude\//g, '.windsurf/');
converted = converted.replace(/\.\/\.claude\//g, './.devin/');
converted = converted.replace(/\.claude\//g, '.devin/');
// Bare forms (no trailing slash) — after slash forms to avoid double-rewrite. // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
// Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore. // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin'); converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin'); converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
// Environment variable name rewrite // Environment variable name rewrite
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR'); converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
// Remove Claude Code-specific bug workarounds before brand replacement // 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()}`; 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 converters ---
// Augment uses a tool set similar to Cursor/Windsurf. // Augment uses a tool set similar to Cursor/Windsurf.
// Config lives in .augment/ (local) and ~/.augment/ (global). // Config lives in .augment/ (local) and ~/.augment/ (global).
@@ -2568,6 +2579,7 @@ export = {
convertClaudeCommandToCursorCommand, convertClaudeCommandToCursorCommand,
convertClaudeToWindsurfMarkdown, convertClaudeToWindsurfMarkdown,
convertClaudeCommandToWindsurfSkill, convertClaudeCommandToWindsurfSkill,
convertClaudeCommandToWindsurfWorkflow,
convertClaudeToAugmentMarkdown, convertClaudeToAugmentMarkdown,
convertClaudeCommandToAugmentSkill, convertClaudeCommandToAugmentSkill,
convertClaudeToTraeMarkdown, convertClaudeToTraeMarkdown,

View File

@@ -133,7 +133,7 @@ export function getProjectInstructionFile(runtime: unknown): string {
/** /**
* Map a canonical runtime id to its on-disk local config directory name * 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`. * fall back to `.claude`.
* *
* Pure runtime-identity projection. Relocated from `bin/install.js` per * 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 === 'codex') return '.codex';
if (runtime === 'antigravity') return '.agents'; if (runtime === 'antigravity') return '.agents';
if (runtime === 'cursor') return '.cursor'; if (runtime === 'cursor') return '.cursor';
if (runtime === 'windsurf') return '.devin'; if (runtime === 'windsurf') return '.windsurf';
if (runtime === 'augment') return '.augment'; if (runtime === 'augment') return '.augment';
if (runtime === 'trae') return '.trae'; if (runtime === 'trae') return '.trae';
if (runtime === 'qwen') return '.qwen'; if (runtime === 'qwen') return '.qwen';

View File

@@ -26,8 +26,8 @@ export const RUNTIME_DIRS: RuntimeDirEntry[] = [
['antigravity', '.gemini/antigravity'], ['antigravity', '.gemini/antigravity'],
['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('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) ['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 workflow dir canonical (#1615; bin/install.js getDirName('windsurf'))
['windsurf', '.windsurf'], // local Windsurf install dir legacy (#1085; backward-compat with pre-#1085 installs) ['windsurf', '.devin'], // local Devin Desktop install dir legacy (#1085; backward-compat)
['gemini', '.gemini'], ['gemini', '.gemini'],
['kilo', '.config/kilo'], ['kilo', '.config/kilo'],
['kilo', '.kilo'], ['kilo', '.kilo'],

View File

@@ -56,13 +56,13 @@ const RUNTIME_META = {
opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') }, opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') },
qwen: { localDir: '.qwen', globalSuffix: '.qwen' }, qwen: { localDir: '.qwen', globalSuffix: '.qwen' },
trae: { localDir: '.trae', globalSuffix: '.trae' }, 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) // Runtimes that emit per-skill files under skills/ (not rules-based or commands-based)
const SKILL_RUNTIMES = [ const SKILL_RUNTIMES = [
'claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity', 'claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity',
'cursor', 'windsurf', 'augment', 'trae', 'qwen', 'codebuddy', 'cursor', 'augment', 'trae', 'qwen', 'codebuddy',
]; ];
// ─── Helper functions ───────────────────────────────────────────────────────── // ─── Helper functions ─────────────────────────────────────────────────────────
@@ -114,7 +114,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [] }) {
const LOCAL_DIR_NAME = { const LOCAL_DIR_NAME = {
claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo', claude: '.claude', opencode: '.opencode', gemini: '.gemini', kilo: '.kilo',
codex: '.codex', copilot: '.github', antigravity: '.agents', cursor: '.cursor', 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: '.', codebuddy: '.codebuddy', cline: '.',
}; };
let configDir; let configDir;

View File

@@ -140,7 +140,7 @@ describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Mod
const SKILLS_RUNTIMES_LAYOUT = [ const SKILLS_RUNTIMES_LAYOUT = [
'claude', 'cursor', 'codex', 'copilot', 'antigravity', 'claude', 'cursor', 'codex', 'copilot', 'antigravity',
'windsurf', 'augment', 'trae', 'qwen', 'kimi', 'codebuddy', 'augment', 'trae', 'qwen', 'kimi', 'codebuddy',
]; ];
const ALL_RUNTIMES_LAYOUT = [ 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)', () => { describe('installRuntimeArtifacts — cline skills (#782)', () => {
test('cline: global install writes gsd-prefixed skill dirs under skills/', (t) => { test('cline: global install writes gsd-prefixed skill dirs under skills/', (t) => {
const configDir = createTempDir('gsd-ial-cline-'); const configDir = createTempDir('gsd-ial-cline-');

View File

@@ -1269,12 +1269,12 @@ describe('install — --devin-desktop CLI flag routes to windsurf runtime (#792)
assert.deepStrictEqual(selectRuntimesFromArgs(['--devin-desktop']), ['windsurf']); 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 // allow-test-rule: runtime-contract-is-the-product
// Reads deployed skill .md files whose text IS the product surface the // Reads deployed workflow .md files whose text IS the product surface the
// Windsurf/Devin Desktop runtime loads at startup (path references, command names). // 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 tmpDir;
let previousCwd; let previousCwd;
@@ -1289,50 +1289,48 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () =>
cleanup(tmpDir); cleanup(tmpDir);
}); });
test('install writes workspace skills under .devin/skills/', () => { test('install writes workspace workflows under .windsurf/workflows/', () => {
const result = install(false, 'windsurf'); const result = install(false, 'windsurf');
const devinDir = path.join(tmpDir, '.devin'); const windsurfDir = path.join(tmpDir, '.windsurf');
assert.strictEqual(result.runtime, 'windsurf'); assert.strictEqual(result.runtime, 'windsurf');
assert.ok(fs.existsSync(devinDir), '.devin/ must be created for local windsurf install'); assert.ok(fs.existsSync(windsurfDir), '.windsurf/ must be created for local windsurf install');
const skillsDir = path.join(devinDir, 'skills'); const workflowsDir = path.join(windsurfDir, 'workflows');
assert.ok(fs.existsSync(skillsDir), '.devin/skills/ must exist after install'); assert.ok(fs.existsSync(workflowsDir), '.windsurf/workflows/ must exist after install');
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true }) const workflowEntries = fs.readdirSync(workflowsDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')); .filter(e => e.isFile() && e.name.startsWith('gsd-') && e.name.endsWith('.md'));
assert.ok(skillEntries.length > 0, 'at least one gsd-* skill must be installed under .devin/skills/'); assert.ok(workflowEntries.length > 0, 'at least one gsd-* workflow must be installed under .windsurf/workflows/');
const firstSkill = path.join(skillsDir, skillEntries[0].name, 'SKILL.md'); assert.ok(fs.existsSync(path.join(workflowsDir, 'gsd-help.md')), 'gsd-help.md workflow must exist');
assert.ok(fs.existsSync(firstSkill), `SKILL.md must exist at ${firstSkill}`);
}); });
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'); install(false, 'windsurf');
const legacyDir = path.join(tmpDir, '.windsurf'); assert.ok(!fs.existsSync(path.join(tmpDir, '.devin', 'skills')),
assert.ok(!fs.existsSync(legacyDir), '.devin/skills must not be created by a fresh install (new installs use .windsurf/workflows)');
'.windsurf/ must not be created by a fresh install (new installs use .devin/)'); 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'); install(false, 'windsurf');
const skillsDir = path.join(tmpDir, '.devin', 'skills'); const workflowsDir = path.join(tmpDir, '.windsurf', 'workflows');
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true }) const workflowEntries = fs.readdirSync(workflowsDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')); .filter(e => e.isFile() && e.name.startsWith('gsd-') && e.name.endsWith('.md'));
assert.ok(skillEntries.length > 0, 'pre-condition: at least one gsd-* skill must be installed'); assert.ok(workflowEntries.length > 0, 'pre-condition: at least one gsd-* workflow must be installed');
for (const skillEntry of skillEntries) { for (const workflowEntry of workflowEntries) {
const skillFile = path.join(skillsDir, skillEntry.name, 'SKILL.md'); const workflowFile = path.join(workflowsDir, workflowEntry.name);
if (!fs.existsSync(skillFile)) continue; const content = fs.readFileSync(workflowFile, 'utf8');
const content = fs.readFileSync(skillFile, 'utf8');
assert.ok( assert.ok(
!content.includes('~/.claude/') && !content.includes('$HOME/.claude/'), content.includes(`${tmpDir}/.windsurf/gsd-core/commands/gsd/`),
`${skillEntry.name}/SKILL.md must not contain ~/.claude/ or $HOME/.claude/ in a local install`, `${workflowEntry.name} must reference the installed canonical command body`,
); );
// Local install must use workspace-relative .devin/ form, not the legacy .windsurf/ form
assert.ok( assert.ok(
!content.includes('~/.windsurf/') && !content.includes('.windsurf/skills/'), !content.includes('/skills/') && !content.includes('SKILL.md'),
`${skillEntry.name}/SKILL.md must not contain bare .windsurf/ path in a local install (use .devin/ instead)`, `${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 homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-'));
const savedHome = process.env.HOME; const savedHome = process.env.HOME;
const savedUserProfile = process.env.USERPROFILE; 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}`, `global windsurf install must go to codeium/windsurf path, got: ${result.configDir}`,
); );
assert.ok( assert.ok(
fs.existsSync(path.join(result.configDir, 'skills')), !fs.existsSync(path.join(result.configDir, 'workflows')),
'global windsurf install must create skills/ under ~/.codeium/windsurf', 'global windsurf install must not create unsupported workflows/ under ~/.codeium/windsurf',
); );
assert.ok( assert.ok(
!fs.existsSync(path.join(homeDir, '.devin')), !fs.existsSync(path.join(result.configDir, 'skills')),
'.devin/ must NOT be created by a global install (global path is ~/.codeium/windsurf)', 'global windsurf install must not create dead skills/ artifacts',
); );
} finally { } finally {
if (savedHome === undefined) delete process.env.HOME; 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 homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-c-'));
const savedHome = process.env.HOME; const savedHome = process.env.HOME;
const savedUserProfile = process.env.USERPROFILE; const savedUserProfile = process.env.USERPROFILE;
@@ -1376,37 +1374,10 @@ describe('windsurf local install writes to .devin/ canonical dir (#1085)', () =>
process.env.USERPROFILE = homeDir; process.env.USERPROFILE = homeDir;
try { try {
const result = install(true, 'windsurf'); const result = install(true, 'windsurf');
const skillsDir = path.join(result.configDir, 'skills'); assert.ok(fs.existsSync(path.join(result.configDir, 'gsd-core', 'workflows', 'update.md')),
if (!fs.existsSync(skillsDir)) return; // no skills emitted — skip 'global windsurf install should still copy shared gsd-core workflow assets');
const skillEntries = fs.readdirSync(skillsDir, { withFileTypes: true }) assert.ok(!fs.existsSync(path.join(result.configDir, 'workflows')),
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')); 'global windsurf install must not write workflow files into an undocumented global path');
// 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)',
);
}
} finally { } finally {
if (savedHome === undefined) delete process.env.HOME; if (savedHome === undefined) delete process.env.HOME;
else process.env.HOME = savedHome; else process.env.HOME = savedHome;

View File

@@ -104,12 +104,9 @@ const GOLDEN = {
], ],
// ── windsurf ───────────────────────────────────────────────────────────────── // ── windsurf ─────────────────────────────────────────────────────────────────
// Old switch: no scope branch → local == global. 5b backfill restores this. 'windsurf/global': [],
'windsurf/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
],
'windsurf/local': [ 'windsurf/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' }, { kind: 'commands', destSubpath: 'workflows', prefix: 'gsd-' },
], ],
// ── augment ────────────────────────────────────────────────────────────────── // ── augment ──────────────────────────────────────────────────────────────────

View File

@@ -134,16 +134,23 @@ describe('resolveRuntimeArtifactLayout — antigravity', () => {
}); });
describe('resolveRuntimeArtifactLayout — windsurf', () => { describe('resolveRuntimeArtifactLayout — windsurf', () => {
test('returns correct layout for windsurf', () => { test('returns local workflow layout for windsurf', () => {
const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR); const layout = resolveRuntimeArtifactLayout('windsurf', FAKE_DIR, 'local');
assert.strictEqual(layout.runtime, 'windsurf'); assert.strictEqual(layout.runtime, 'windsurf');
assert.strictEqual(layout.configDir, FAKE_DIR); assert.strictEqual(layout.configDir, FAKE_DIR);
assert.strictEqual(layout.kinds.length, 1); assert.strictEqual(layout.kinds.length, 1);
assert.strictEqual(layout.kinds[0].kind, 'skills'); assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[0].destSubpath, 'skills'); assert.strictEqual(layout.kinds[0].destSubpath, 'workflows');
assert.strictEqual(layout.kinds[0].prefix, 'gsd-'); assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
assert.strictEqual(typeof layout.kinds[0].stage, 'function'); 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', () => { describe('resolveRuntimeArtifactLayout — augment', () => {

View File

@@ -13,6 +13,7 @@ const assert = require('node:assert/strict');
const { const {
convertClaudeCommandToWindsurfSkill, convertClaudeCommandToWindsurfSkill,
convertClaudeCommandToWindsurfWorkflow,
convertClaudeAgentToWindsurfAgent, convertClaudeAgentToWindsurfAgent,
convertClaudeToWindsurfMarkdown, convertClaudeToWindsurfMarkdown,
} = require('../bin/install.js'); } = 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
---
<objective>
Test body
</objective>
`;
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', () => { describe('convertClaudeAgentToWindsurfAgent', () => {
test('converts agent frontmatter with unquoted name', () => { test('converts agent frontmatter with unquoted name', () => {
const input = `--- const input = `---
@@ -105,17 +129,17 @@ describe('convertClaudeToWindsurfMarkdown', () => {
assert.ok(!result.includes('Claude Code'), 'original brand removed'); 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 input = 'See `CLAUDE.md` for configuration. Also check ./CLAUDE.md file.';
const result = convertClaudeToWindsurfMarkdown(input); const result = convertClaudeToWindsurfMarkdown(input);
assert.ok(result.includes('.devin/rules'), 'CLAUDE.md replaced with .devin/rules (#1085)'); assert.ok(result.includes('.windsurf/rules'), 'CLAUDE.md replaced with .windsurf/rules');
assert.ok(!result.includes('.devin/rules/'), 'no trailing slash (Node v25 compat)'); 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 input = 'Skills are stored in .claude/skills/ directory.';
const result = convertClaudeToWindsurfMarkdown(input); 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(', () => { test('replaces Bash( with Shell( and Edit( with StrReplace(', () => {