Merge pull request #1394 from Tibsfox/fix/issue-triage-batch-1392-1391-1389

fix: trailing slash, slug newlines, and duplicate command registration
This commit is contained in:
Tom Boucher
2026-04-01 12:12:37 -04:00
committed by GitHub
8 changed files with 24 additions and 74 deletions

View File

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

View File

@@ -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"
```

View File

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

View File

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

View File

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

View File

@@ -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 <name>` | Create a new workstream |
| `status <name>` | Detailed status for one workstream |
| `switch <name>` | Set active workstream |
| `progress` | Progress summary across all workstreams |
| `complete <name>` | Archive a completed workstream |
| `resume <name>` | 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 <name> --raw --cwd "$CWD"`
After creation, display the new workstream path and suggest next steps:
- `/gsd:new-milestone --ws <name>` to set up the milestone
### status
Run: `node "$GSD_TOOLS" workstream status <name> --raw --cwd "$CWD"`
Display detailed phase breakdown and state information.
### switch
Run: `node "$GSD_TOOLS" workstream set <name> --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 <name> --raw --cwd "$CWD"`
Archive the workstream to milestones/.
### resume
Set the workstream as active and suggest `/gsd:resume-work --ws <name>`.
## 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.

View File

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

View File

@@ -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/', () => {