From 69e104dffcbf1e19ae27dcda31094bf87e9580ed Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Thu, 26 Mar 2026 01:13:46 -0700 Subject: [PATCH 1/3] fix(windsurf): remove trailing slash from .windsurf/rules path Node v25 preserves trailing slashes in path.join, causing writeFileSync to fail with ENOENT when the converted path ends in '/'. Affects all Windsurf users on Node v25+. Fixes gsd-build/get-shit-done#1392 Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/install.js | 10 +++++----- tests/windsurf-conversion.test.cjs | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bin/install.js b/bin/install.js index c65b7ac52..a8dee4cb7 100755 --- a/bin/install.js +++ b/bin/install.js @@ -919,10 +919,10 @@ function convertClaudeToWindsurfMarkdown(content) { 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 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\.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/'); // Remove Claude Code-specific bug workarounds before brand replacement converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); @@ -3127,7 +3127,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand let jsContent = fs.readFileSync(srcPath, 'utf8'); jsContent = jsContent.replace(/gsd:/gi, 'gsd-'); jsContent = jsContent.replace(/\.claude\/skills\//g, '.windsurf/skills/'); - jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules/'); + jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules'); jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf'); fs.writeFileSync(destPath, jsContent); } else { diff --git a/tests/windsurf-conversion.test.cjs b/tests/windsurf-conversion.test.cjs index 06834d5c9..3df82ed1b 100644 --- a/tests/windsurf-conversion.test.cjs +++ b/tests/windsurf-conversion.test.cjs @@ -105,10 +105,11 @@ describe('convertClaudeToWindsurfMarkdown', () => { assert.ok(!result.includes('Claude Code'), 'original brand removed'); }); - test('replaces CLAUDE.md with .windsurf/rules/', () => { + 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('.windsurf/rules/'), 'CLAUDE.md replaced'); + assert.ok(result.includes('.windsurf/rules'), 'CLAUDE.md replaced'); + assert.ok(!result.includes('.windsurf/rules/'), 'no trailing slash (Node v25 compat)'); }); test('replaces .claude/skills/ with .windsurf/skills/', () => { From 9647c719c4a693a4e86c3371f8b1f42c1bda63b0 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Thu, 26 Mar 2026 01:16:01 -0700 Subject: [PATCH 2/3] fix(slug): add --raw flag to generate-slug callers and cap length add-backlog and thread commands called generate-slug without --raw, capturing JSON output (with newlines) as the directory name. Also cap slugs at 60 chars to prevent absurdly long directory names. Fixes gsd-build/get-shit-done#1391 Co-Authored-By: Claude Opus 4.6 (1M context) --- commands/gsd/add-backlog.md | 2 +- commands/gsd/thread.md | 2 +- get-shit-done/bin/lib/commands.cjs | 3 ++- get-shit-done/bin/lib/core.cjs | 2 +- tests/core.test.cjs | 11 +++++++++++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/commands/gsd/add-backlog.md b/commands/gsd/add-backlog.md index a144fb975..767a51bab 100644 --- a/commands/gsd/add-backlog.md +++ b/commands/gsd/add-backlog.md @@ -29,7 +29,7 @@ the normal phase sequence and accumulate context over time. 3. **Create the phase directory:** ```bash - SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS") + SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS" --raw) mkdir -p ".planning/phases/${NEXT}-${SLUG}" touch ".planning/phases/${NEXT}-${SLUG}/.gitkeep" ``` diff --git a/commands/gsd/thread.md b/commands/gsd/thread.md index fe921184b..adbdca5f4 100644 --- a/commands/gsd/thread.md +++ b/commands/gsd/thread.md @@ -62,7 +62,7 @@ Create a new thread: 1. Generate slug from description: ```bash - SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS") + SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS" --raw) ``` 2. Create the threads directory if needed: diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 425199dde..173611d13 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -16,7 +16,8 @@ function cmdGenerateSlug(text, raw) { const slug = text .toLowerCase() .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); + .replace(/^-+|-+$/g, '') + .substring(0, 60); const result = { slug }; output(result, raw, slug); diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index e509e849a..0e77c572b 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -1061,7 +1061,7 @@ function pathExistsInternal(cwd, targetPath) { function generateSlugInternal(text) { if (!text) return null; - return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 60); } function getMilestoneInfo(cwd) { diff --git a/tests/core.test.cjs b/tests/core.test.cjs index 4211b3f8e..ccea7313c 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -366,6 +366,17 @@ describe('generateSlugInternal', () => { test('returns null for empty string', () => { assert.strictEqual(generateSlugInternal(''), null); }); + + test('strips newlines and control characters', () => { + assert.strictEqual(generateSlugInternal('hello\nworld'), 'hello-world'); + assert.strictEqual(generateSlugInternal('tab\there'), 'tab-here'); + }); + + test('truncates to 60 characters', () => { + const long = 'a'.repeat(100); + const result = generateSlugInternal(long); + assert.ok(result.length <= 60, `slug should be <=60 chars, got ${result.length}`); + }); }); // ─── normalizePhaseName / comparePhaseNum ────────────────────────────────────── From b5cbd47373648ecc6e90b360654ce64ae96ce2b6 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Thu, 26 Mar 2026 01:16:44 -0700 Subject: [PATCH 3/3] fix(commands): remove duplicate workstreams.md from plugin directory get-shit-done/commands/gsd/workstreams.md was identical to commands/gsd/workstreams.md, causing Claude Code to register every gsd:* command twice as gsd:gsd:* when scanning plugin directories. Fixes gsd-build/get-shit-done#1389 Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/commands/gsd/workstreams.md | 63 ----------------------- 1 file changed, 63 deletions(-) delete mode 100644 get-shit-done/commands/gsd/workstreams.md diff --git a/get-shit-done/commands/gsd/workstreams.md b/get-shit-done/commands/gsd/workstreams.md deleted file mode 100644 index 1a9191036..000000000 --- a/get-shit-done/commands/gsd/workstreams.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -description: Manage parallel workstreams — list, create, switch, status, progress, complete, and resume ---- - -# /gsd:workstreams - -Manage parallel workstreams for concurrent milestone work. - -## Usage - -`/gsd:workstreams [subcommand] [args]` - -### Subcommands - -| Command | Description | -|---------|-------------| -| `list` | List all workstreams with status | -| `create ` | Create a new workstream | -| `status ` | Detailed status for one workstream | -| `switch ` | Set active workstream | -| `progress` | Progress summary across all workstreams | -| `complete ` | Archive a completed workstream | -| `resume ` | Resume work in a workstream | - -## Step 1: Parse Subcommand - -Parse the user's input to determine which workstream operation to perform. -If no subcommand given, default to `list`. - -## Step 2: Execute Operation - -### list -Run: `node "$GSD_TOOLS" workstream list --raw --cwd "$CWD"` -Display the workstreams in a table format showing name, status, current phase, and progress. - -### create -Run: `node "$GSD_TOOLS" workstream create --raw --cwd "$CWD"` -After creation, display the new workstream path and suggest next steps: -- `/gsd:new-milestone --ws ` to set up the milestone - -### status -Run: `node "$GSD_TOOLS" workstream status --raw --cwd "$CWD"` -Display detailed phase breakdown and state information. - -### switch -Run: `node "$GSD_TOOLS" workstream set --raw --cwd "$CWD"` -Also set `GSD_WORKSTREAM` env var for the current session. - -### progress -Run: `node "$GSD_TOOLS" workstream progress --raw --cwd "$CWD"` -Display a progress overview across all workstreams. - -### complete -Run: `node "$GSD_TOOLS" workstream complete --raw --cwd "$CWD"` -Archive the workstream to milestones/. - -### resume -Set the workstream as active and suggest `/gsd:resume-work --ws `. - -## Step 3: Display Results - -Format the JSON output from gsd-tools into a human-readable display. -Include the `${GSD_WS}` flag in any routing suggestions.