diff --git a/bin/install.js b/bin/install.js index f1f919ea1..843832c14 100755 --- a/bin/install.js +++ b/bin/install.js @@ -921,10 +921,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, ''); @@ -3129,7 +3129,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/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/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. 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 ────────────────────────────────────── 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/', () => {