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 ──────────────────────────────────────