diff --git a/commands/gsd/list-workspaces.md b/commands/gsd/list-workspaces.md
new file mode 100644
index 000000000..932e46dd1
--- /dev/null
+++ b/commands/gsd/list-workspaces.md
@@ -0,0 +1,19 @@
+---
+name: gsd:list-workspaces
+description: List active GSD workspaces and their status
+allowed-tools:
+ - Bash
+ - Read
+---
+
+Scan `~/gsd-workspaces/` for workspace directories containing `WORKSPACE.md` manifests. Display a summary table with name, path, repo count, strategy, and GSD project status.
+
+
+
+@~/.claude/get-shit-done/workflows/list-workspaces.md
+@~/.claude/get-shit-done/references/ui-brand.md
+
+
+
+Execute the list-workspaces workflow from @~/.claude/get-shit-done/workflows/list-workspaces.md end-to-end.
+
diff --git a/commands/gsd/new-workspace.md b/commands/gsd/new-workspace.md
new file mode 100644
index 000000000..e340b8037
--- /dev/null
+++ b/commands/gsd/new-workspace.md
@@ -0,0 +1,44 @@
+---
+name: gsd:new-workspace
+description: Create an isolated workspace with repo copies and independent .planning/
+argument-hint: "--name [--repos repo1,repo2] [--path /target] [--strategy worktree|clone] [--branch name] [--auto]"
+allowed-tools:
+ - Read
+ - Bash
+ - Write
+ - AskUserQuestion
+---
+
+**Flags:**
+- `--name` (required) — Workspace name
+- `--repos` — Comma-separated repo paths or names. If omitted, interactive selection from child git repos in cwd
+- `--path` — Target directory. Defaults to `~/gsd-workspaces/`
+- `--strategy` — `worktree` (default, lightweight) or `clone` (fully independent)
+- `--branch` — Branch to checkout. Defaults to `workspace/`
+- `--auto` — Skip interactive questions, use defaults
+
+
+
+Create a physical workspace directory containing copies of specified git repos (as worktrees or clones) with an independent `.planning/` directory for isolated GSD sessions.
+
+**Use cases:**
+- Multi-repo orchestration: work on a subset of repos in parallel with isolated GSD state
+- Feature branch isolation: create a worktree of the current repo with its own `.planning/`
+
+**Creates:**
+- `/WORKSPACE.md` — workspace manifest
+- `/.planning/` — independent planning directory
+- `//` — git worktree or clone for each specified repo
+
+**After this command:** `cd` into the workspace and run `/gsd:new-project` to initialize GSD.
+
+
+
+@~/.claude/get-shit-done/workflows/new-workspace.md
+@~/.claude/get-shit-done/references/ui-brand.md
+
+
+
+Execute the new-workspace workflow from @~/.claude/get-shit-done/workflows/new-workspace.md end-to-end.
+Preserve all workflow gates (validation, approvals, commits, routing).
+
diff --git a/commands/gsd/remove-workspace.md b/commands/gsd/remove-workspace.md
new file mode 100644
index 000000000..2b9855ef3
--- /dev/null
+++ b/commands/gsd/remove-workspace.md
@@ -0,0 +1,26 @@
+---
+name: gsd:remove-workspace
+description: Remove a GSD workspace and clean up worktrees
+argument-hint: ""
+allowed-tools:
+ - Bash
+ - Read
+ - AskUserQuestion
+---
+
+**Arguments:**
+- `` (required) — Name of the workspace to remove
+
+
+
+Remove a workspace directory after confirmation. For worktree strategy, runs `git worktree remove` for each member repo first. Refuses if any repo has uncommitted changes.
+
+
+
+@~/.claude/get-shit-done/workflows/remove-workspace.md
+@~/.claude/get-shit-done/references/ui-brand.md
+
+
+
+Execute the remove-workspace workflow from @~/.claude/get-shit-done/workflows/remove-workspace.md end-to-end.
+
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index a9756c29a..a55619ff3 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -32,6 +32,62 @@ Initialize a new project with deep context gathering.
---
+### `/gsd:new-workspace`
+
+Create an isolated workspace with repo copies and independent `.planning/` directory.
+
+| Flag | Description |
+|------|-------------|
+| `--name ` | Workspace name (required) |
+| `--repos repo1,repo2` | Comma-separated repo paths or names |
+| `--path /target` | Target directory (default: `~/gsd-workspaces/`) |
+| `--strategy worktree\|clone` | Copy strategy (default: `worktree`) |
+| `--branch ` | Branch to checkout (default: `workspace/`) |
+| `--auto` | Skip interactive questions |
+
+**Use cases:**
+- Multi-repo: work on a subset of repos with isolated GSD state
+- Feature isolation: `--repos .` creates a worktree of the current repo
+
+**Produces:** `WORKSPACE.md`, `.planning/`, repo copies (worktrees or clones)
+
+```bash
+/gsd:new-workspace --name feature-b --repos hr-ui,ZeymoAPI
+/gsd:new-workspace --name feature-b --repos . --strategy worktree # Same-repo isolation
+/gsd:new-workspace --name spike --repos api,web --strategy clone # Full clones
+```
+
+---
+
+### `/gsd:list-workspaces`
+
+List active GSD workspaces and their status.
+
+**Scans:** `~/gsd-workspaces/` for `WORKSPACE.md` manifests
+**Shows:** Name, repo count, strategy, GSD project status
+
+```bash
+/gsd:list-workspaces
+```
+
+---
+
+### `/gsd:remove-workspace`
+
+Remove a workspace and clean up git worktrees.
+
+| Argument | Required | Description |
+|----------|----------|-------------|
+| `` | Yes | Workspace name to remove |
+
+**Safety:** Refuses removal if any repo has uncommitted changes. Requires name confirmation.
+
+```bash
+/gsd:remove-workspace feature-b
+```
+
+---
+
### `/gsd:discuss-phase`
Capture implementation decisions before planning.
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
index d5948897f..6d0bd5b5f 100644
--- a/docs/USER-GUIDE.md
+++ b/docs/USER-GUIDE.md
@@ -625,6 +625,31 @@ claude --dangerously-skip-permissions
/gsd:remove-phase 7 # Descope phase 7 and renumber
```
+### Multi-Project Workspaces
+
+Work on multiple repos or features in parallel with isolated GSD state.
+
+```bash
+# Create a workspace with repos from your monorepo
+/gsd:new-workspace --name feature-b --repos hr-ui,ZeymoAPI
+
+# Feature branch isolation — worktree of current repo with its own .planning/
+/gsd:new-workspace --name feature-b --repos .
+
+# Then cd into the workspace and initialize GSD
+cd ~/gsd-workspaces/feature-b
+/gsd:new-project
+
+# List and manage workspaces
+/gsd:list-workspaces
+/gsd:remove-workspace feature-b
+```
+
+Each workspace gets:
+- Its own `.planning/` directory (fully independent from source repos)
+- Git worktrees (default) or clones of specified repos
+- A `WORKSPACE.md` manifest tracking member repos
+
---
## Troubleshooting
diff --git a/docs/superpowers/specs/2026-03-20-multi-project-workspaces-design.md b/docs/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
new file mode 100644
index 000000000..1953dd5c2
--- /dev/null
+++ b/docs/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
@@ -0,0 +1,185 @@
+# Multi-Project Workspaces (`/gsd:new-workspace`)
+
+**Issue:** #1241
+**Date:** 2026-03-20
+**Status:** Approved
+
+## Problem
+
+GSD is tied to one `.planning/` directory per working directory. Users with multiple independent projects (monorepo-style setups with 20+ child repos) or users needing feature branch isolation in the same repo cannot run parallel GSD sessions without manual cloning and state management.
+
+## Solution
+
+Three new commands that create, list, and remove **physical workspace directories** — each containing repo copies (git worktrees or clones) and an independent `.planning/` directory.
+
+This covers two use cases:
+- **Multi-repo orchestration (A):** Workspace spanning multiple repos from a parent directory
+- **Feature branch isolation (B):** Workspace containing a worktree of the current repo (special case of A where `--repos .`)
+
+## Commands
+
+### `/gsd:new-workspace`
+
+Creates a workspace directory with repo copies and its own `.planning/`.
+
+```
+/gsd:new-workspace --name feature-b --repos hr-ui,ZeymoAPI --path ~/workspaces/feature-b
+/gsd:new-workspace --name feature-b --repos . --strategy worktree # same-repo isolation
+```
+
+**Arguments:**
+
+| Flag | Required | Default | Description |
+|------|----------|---------|-------------|
+| `--name` | Yes | — | Workspace name |
+| `--repos` | No | Interactive selection | Comma-separated repo paths or names |
+| `--path` | No | `~/gsd-workspaces/` | Target directory |
+| `--strategy` | No | `worktree` | `worktree` (lightweight, shared .git) or `clone` (fully independent) |
+| `--branch` | No | `workspace/` | Branch to checkout |
+| `--auto` | No | false | Skip interactive questions, use defaults |
+
+### `/gsd:list-workspaces`
+
+Scans `~/gsd-workspaces/*/WORKSPACE.md` for workspace manifests. Displays table with name, path, repo count, GSD status (has PROJECT.md, current phase).
+
+### `/gsd:remove-workspace`
+
+Removes a workspace directory after confirmation. For worktree strategy, runs `git worktree remove` for each member repo first. Refuses if any repo has uncommitted changes.
+
+## Directory Structure
+
+```
+~/gsd-workspaces/feature-b/ # workspace root
+├── WORKSPACE.md # manifest
+├── .planning/ # independent GSD planning directory
+│ ├── PROJECT.md # (if user ran /gsd:new-project)
+│ ├── STATE.md
+│ └── config.json
+├── hr-ui/ # git worktree of source repo
+│ └── (repo contents on workspace/feature-b branch)
+└── ZeymoAPI/ # git worktree of source repo
+ └── (repo contents on workspace/feature-b branch)
+```
+
+Key properties:
+- `.planning/` is at the workspace root, not inside any individual repo
+- Each repo is a peer directory under the workspace root
+- `WORKSPACE.md` is the only GSD-specific file at the root (besides `.planning/`)
+- For `--strategy clone`, same structure but repos are full clones
+
+## WORKSPACE.md Format
+
+```markdown
+# Workspace: feature-b
+
+Created: 2026-03-20
+Strategy: worktree
+
+## Member Repos
+
+| Repo | Source | Branch | Strategy |
+|------|--------|--------|----------|
+| hr-ui | /root/source/repos/hr-ui | workspace/feature-b | worktree |
+| ZeymoAPI | /root/source/repos/ZeymoAPI | workspace/feature-b | worktree |
+
+## Notes
+
+[User can add context about what this workspace is for]
+```
+
+## Workflow
+
+### `/gsd:new-workspace` Workflow Steps
+
+1. **Setup** — Call `init new-workspace`, parse JSON context
+2. **Gather inputs** — If `--name`/`--repos`/`--path` not provided, ask interactively. For repos, show child `.git` directories in cwd as options
+3. **Validate** — Target path doesn't exist (or is empty). Source repos exist and are git repos
+4. **Create workspace directory** — `mkdir -p `
+5. **Copy repos** — For each repo:
+ - Worktree: `git worktree add / -b workspace/`
+ - Clone: `git clone /`
+6. **Write WORKSPACE.md** — Manifest with source paths, strategy, branch
+7. **Initialize .planning/** — `mkdir -p /.planning`
+8. **Offer /gsd:new-project** — Ask if user wants to run project initialization in the new workspace
+9. **Commit** — If commit_docs enabled, atomic commit of WORKSPACE.md
+10. **Done** — Print workspace path and next steps
+
+### Init Function (`cmdInitNewWorkspace`)
+
+Detects:
+- Child git repos in cwd (for interactive repo selection)
+- Whether target path already exists
+- Whether source repos have uncommitted changes
+- Whether `git worktree` is available
+- Default workspace base dir (`~/gsd-workspaces/`)
+
+Returns JSON with flags for workflow gating.
+
+## Error Handling
+
+### Validation Errors (Block Creation)
+
+- **Target path exists and is non-empty** — Error with suggestion to pick a different name/path
+- **Source repo path doesn't exist or isn't a git repo** — Error listing which repos failed
+- **`git worktree add` fails** (e.g., branch exists) — Fall back to `workspace/-` branch, or error if that also fails
+
+### Graceful Handling
+
+- **Source repo has uncommitted changes** — Warn but allow (worktrees checkout the branch fresh, don't copy working directory state)
+- **Partial failure in multi-repo workspace** — Create workspace with repos that succeeded, report failures, write partial WORKSPACE.md
+- **`--repos .` (current repo, case B)** — Detect repo name from directory name or git remote, use as subdirectory name
+
+### Remove-Workspace Safety
+
+- **Uncommitted changes in workspace repos** — Refuse removal, print which repos have changes
+- **Worktree removal fails** (e.g., source repo deleted) — Warn and continue with directory cleanup
+- **Confirmation** — Require explicit confirmation with workspace name typed out
+
+### List-Workspaces Edge Cases
+
+- **`~/gsd-workspaces/` doesn't exist** — "No workspaces found"
+- **WORKSPACE.md exists but repos inside are gone** — Show workspace, mark repos as missing
+
+## Testing
+
+### Unit Tests (`tests/workspace.test.cjs`)
+
+1. `cmdInitNewWorkspace` returns correct JSON — detects child git repos, validates target path, detects git worktree availability
+2. WORKSPACE.md generation — correct format with repo table, strategy, date
+3. Repo discovery — identifies `.git` directories in cwd children, skips non-git directories and files
+4. Validation — rejects existing non-empty target paths, rejects non-git source paths
+
+### Integration Tests (same file)
+
+5. Worktree creation — creates workspace, verifies repo directories are valid git worktrees
+6. Clone creation — creates workspace, verifies repos are independent clones
+7. List workspaces — creates two workspaces, verifies list output includes both
+8. Remove workspace — creates workspace with worktrees, removes it, verifies cleanup
+9. Partial failure — one valid repo + one invalid path, workspace created with valid repo only
+
+All tests use temp directories and clean up after themselves. Follow existing `node:test` + `node:assert` patterns.
+
+## Implementation Files
+
+| Component | Path |
+|-----------|------|
+| Command: new-workspace | `commands/gsd/new-workspace.md` |
+| Command: list-workspaces | `commands/gsd/list-workspaces.md` |
+| Command: remove-workspace | `commands/gsd/remove-workspace.md` |
+| Workflow: new-workspace | `get-shit-done/workflows/new-workspace.md` |
+| Workflow: list-workspaces | `get-shit-done/workflows/list-workspaces.md` |
+| Workflow: remove-workspace | `get-shit-done/workflows/remove-workspace.md` |
+| Init function | `get-shit-done/bin/lib/init.cjs` (add `cmdInitNewWorkspace`, `cmdInitListWorkspaces`, `cmdInitRemoveWorkspace`) |
+| Routing | `get-shit-done/bin/gsd-tools.cjs` (add cases to init switch) |
+| Tests | `tests/workspace.test.cjs` |
+
+## Design Decisions
+
+| Decision | Rationale |
+|----------|-----------|
+| Physical directories over logical registry | Filesystem is source of truth — matches GSD's existing cwd-based detection pattern |
+| Worktree as default strategy | Lightweight (shared .git objects), fast to create, easy to clean up |
+| `.planning/` at workspace root | Gives full isolation from individual repo planning. Each workspace is an independent GSD project |
+| No central registry | Avoids state drift. `list-workspaces` scans the filesystem directly |
+| Case B as special case of A | `--repos .` reuses the same machinery, no special feature-branch code needed |
+| Default path `~/gsd-workspaces/` | Predictable location for `list-workspaces` to scan, keeps workspaces out of source repos |
diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs
index c15104f0b..18fc219df 100755
--- a/get-shit-done/bin/gsd-tools.cjs
+++ b/get-shit-done/bin/gsd-tools.cjs
@@ -651,8 +651,17 @@ async function main() {
case 'progress':
init.cmdInitProgress(cwd, raw);
break;
+ case 'new-workspace':
+ init.cmdInitNewWorkspace(cwd, raw);
+ break;
+ case 'list-workspaces':
+ init.cmdInitListWorkspaces(cwd, raw);
+ break;
+ case 'remove-workspace':
+ init.cmdInitRemoveWorkspace(cwd, args[2], raw);
+ break;
default:
- error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress`);
+ error(`Unknown init workflow: ${workflow}\nAvailable: execute-phase, plan-phase, new-project, new-milestone, quick, resume, verify-work, phase-op, todos, milestone-op, map-codebase, progress, new-workspace, list-workspaces, remove-workspace`);
}
break;
}
diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs
index d87302b51..5b8d5b8f2 100644
--- a/get-shit-done/bin/lib/init.cjs
+++ b/get-shit-done/bin/lib/init.cjs
@@ -896,6 +896,165 @@ function cmdInitProgress(cwd, raw) {
output(withProjectRoot(cwd, result), raw);
}
+/**
+ * Detect child git repos in a directory (one level deep).
+ * Returns array of { name, path, has_uncommitted } objects.
+ */
+function detectChildRepos(dir) {
+ const repos = [];
+ let entries;
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return repos; }
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ if (entry.name.startsWith('.')) continue;
+ const fullPath = path.join(dir, entry.name);
+ const gitDir = path.join(fullPath, '.git');
+ if (fs.existsSync(gitDir)) {
+ let hasUncommitted = false;
+ try {
+ const status = execSync('git status --porcelain', { cwd: fullPath, encoding: 'utf8', timeout: 5000 });
+ hasUncommitted = status.trim().length > 0;
+ } catch { /* best-effort */ }
+ repos.push({ name: entry.name, path: fullPath, has_uncommitted: hasUncommitted });
+ }
+ }
+ return repos;
+}
+
+function cmdInitNewWorkspace(cwd, raw) {
+ const homedir = process.env.HOME || require('os').homedir();
+ const defaultBase = path.join(homedir, 'gsd-workspaces');
+
+ // Detect child git repos for interactive selection
+ const childRepos = detectChildRepos(cwd);
+
+ // Check if git worktree is available
+ let worktreeAvailable = false;
+ try {
+ execSync('git --version', { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
+ worktreeAvailable = true;
+ } catch { /* no git at all */ }
+
+ const result = {
+ default_workspace_base: defaultBase,
+ child_repos: childRepos,
+ child_repo_count: childRepos.length,
+ worktree_available: worktreeAvailable,
+ is_git_repo: pathExistsInternal(cwd, '.git'),
+ cwd_repo_name: path.basename(cwd),
+ };
+
+ output(withProjectRoot(cwd, result), raw);
+}
+
+function cmdInitListWorkspaces(cwd, raw) {
+ const homedir = process.env.HOME || require('os').homedir();
+ const defaultBase = path.join(homedir, 'gsd-workspaces');
+
+ const workspaces = [];
+ if (fs.existsSync(defaultBase)) {
+ let entries;
+ try { entries = fs.readdirSync(defaultBase, { withFileTypes: true }); } catch { entries = []; }
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ const wsPath = path.join(defaultBase, entry.name);
+ const manifestPath = path.join(wsPath, 'WORKSPACE.md');
+ if (!fs.existsSync(manifestPath)) continue;
+
+ let repoCount = 0;
+ let hasProject = false;
+ let strategy = 'unknown';
+ try {
+ const manifest = fs.readFileSync(manifestPath, 'utf8');
+ const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
+ if (strategyMatch) strategy = strategyMatch[1].trim();
+ // Count table rows (lines starting with |, excluding header and separator)
+ const tableRows = manifest.split('\n').filter(l => l.match(/^\|\s*\w/) && !l.includes('Repo') && !l.includes('---'));
+ repoCount = tableRows.length;
+ } catch { /* best-effort */ }
+ hasProject = fs.existsSync(path.join(wsPath, '.planning', 'PROJECT.md'));
+
+ workspaces.push({
+ name: entry.name,
+ path: wsPath,
+ repo_count: repoCount,
+ strategy,
+ has_project: hasProject,
+ });
+ }
+ }
+
+ const result = {
+ workspace_base: defaultBase,
+ workspaces,
+ workspace_count: workspaces.length,
+ };
+
+ output(result, raw);
+}
+
+function cmdInitRemoveWorkspace(cwd, name, raw) {
+ const homedir = process.env.HOME || require('os').homedir();
+ const defaultBase = path.join(homedir, 'gsd-workspaces');
+
+ if (!name) {
+ error('workspace name required for init remove-workspace');
+ }
+
+ const wsPath = path.join(defaultBase, name);
+ const manifestPath = path.join(wsPath, 'WORKSPACE.md');
+
+ if (!fs.existsSync(wsPath)) {
+ error(`Workspace not found: ${wsPath}`);
+ }
+
+ // Parse manifest for repo info
+ const repos = [];
+ let strategy = 'unknown';
+ if (fs.existsSync(manifestPath)) {
+ try {
+ const manifest = fs.readFileSync(manifestPath, 'utf8');
+ const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
+ if (strategyMatch) strategy = strategyMatch[1].trim();
+
+ // Parse table rows for repo names and source paths
+ const lines = manifest.split('\n');
+ for (const line of lines) {
+ const match = line.match(/^\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|$/);
+ if (match && match[1] !== 'Repo' && !match[1].includes('---')) {
+ repos.push({ name: match[1], source: match[2], branch: match[3], strategy: match[4] });
+ }
+ }
+ } catch { /* best-effort */ }
+ }
+
+ // Check for uncommitted changes in workspace repos
+ const dirtyRepos = [];
+ for (const repo of repos) {
+ const repoPath = path.join(wsPath, repo.name);
+ if (!fs.existsSync(repoPath)) continue;
+ try {
+ const status = execSync('git status --porcelain', { cwd: repoPath, encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
+ if (status.trim().length > 0) {
+ dirtyRepos.push(repo.name);
+ }
+ } catch { /* best-effort */ }
+ }
+
+ const result = {
+ workspace_name: name,
+ workspace_path: wsPath,
+ has_manifest: fs.existsSync(manifestPath),
+ strategy,
+ repos,
+ repo_count: repos.length,
+ dirty_repos: dirtyRepos,
+ has_dirty_repos: dirtyRepos.length > 0,
+ };
+
+ output(result, raw);
+}
+
module.exports = {
cmdInitExecutePhase,
cmdInitPlanPhase,
@@ -909,4 +1068,8 @@ module.exports = {
cmdInitMilestoneOp,
cmdInitMapCodebase,
cmdInitProgress,
+ cmdInitNewWorkspace,
+ cmdInitListWorkspaces,
+ cmdInitRemoveWorkspace,
+ detectChildRepos,
};
diff --git a/get-shit-done/workflows/list-workspaces.md b/get-shit-done/workflows/list-workspaces.md
new file mode 100644
index 000000000..9a3cbd6a0
--- /dev/null
+++ b/get-shit-done/workflows/list-workspaces.md
@@ -0,0 +1,56 @@
+
+List all GSD workspaces found in ~/gsd-workspaces/ with their status.
+
+
+
+Read all files referenced by the invoking prompt's execution_context before starting.
+
+
+
+
+## 1. Setup
+
+```bash
+INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init list-workspaces)
+if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
+```
+
+Parse JSON for: `workspace_base`, `workspaces`, `workspace_count`.
+
+## 2. Display
+
+**If `workspace_count` is 0:**
+
+```
+No workspaces found in ~/gsd-workspaces/
+
+Create one with:
+ /gsd:new-workspace --name my-workspace --repos repo1,repo2
+```
+
+Done.
+
+**If workspaces exist:**
+
+Display a table:
+
+```
+GSD Workspaces (~/gsd-workspaces/)
+
+| Name | Repos | Strategy | GSD Project |
+|------|-------|----------|-------------|
+| feature-a | 3 | worktree | Yes |
+| feature-b | 2 | clone | No |
+
+Manage:
+ cd ~/gsd-workspaces/ # Enter a workspace
+ /gsd:remove-workspace # Remove a workspace
+```
+
+For each workspace, show:
+- **Name** — directory name
+- **Repos** — count from init data
+- **Strategy** — from WORKSPACE.md
+- **GSD Project** — whether `.planning/PROJECT.md` exists (Yes/No)
+
+
diff --git a/get-shit-done/workflows/new-workspace.md b/get-shit-done/workflows/new-workspace.md
new file mode 100644
index 000000000..35eb3692b
--- /dev/null
+++ b/get-shit-done/workflows/new-workspace.md
@@ -0,0 +1,237 @@
+
+Create an isolated workspace directory with git repo copies (worktrees or clones) and an independent `.planning/` directory. Supports multi-repo orchestration and single-repo feature branch isolation.
+
+
+
+Read all files referenced by the invoking prompt's execution_context before starting.
+
+
+
+
+## 1. Setup
+
+**MANDATORY FIRST STEP — Execute init command:**
+
+```bash
+INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-workspace)
+if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
+```
+
+Parse JSON for: `default_workspace_base`, `child_repos`, `child_repo_count`, `worktree_available`, `is_git_repo`, `cwd_repo_name`, `project_root`.
+
+## 2. Parse Arguments
+
+Extract from $ARGUMENTS:
+- `--name` → `WORKSPACE_NAME` (required)
+- `--repos` → `REPO_LIST` (comma-separated paths or names)
+- `--path` → `TARGET_PATH` (defaults to `$default_workspace_base/$WORKSPACE_NAME`)
+- `--strategy` → `STRATEGY` (defaults to `worktree`)
+- `--branch` → `BRANCH_NAME` (defaults to `workspace/$WORKSPACE_NAME`)
+- `--auto` → skip interactive questions
+
+**If `--name` is missing and not `--auto`:**
+
+Use AskUserQuestion:
+- header: "Workspace Name"
+- question: "What should this workspace be called?"
+- requireAnswer: true
+
+## 3. Select Repos
+
+**If `--repos` is provided:** Parse comma-separated values. For each value:
+- If it's an absolute path, use it directly
+- If it's a relative path or name, resolve against `$project_root`
+- Special case: `.` means current repo (use `$project_root`, name it `$cwd_repo_name`)
+
+**If `--repos` is NOT provided and not `--auto`:**
+
+**If `child_repo_count` > 0:**
+
+Present child repos for selection:
+
+Use AskUserQuestion:
+- header: "Select Repos"
+- question: "Which repos should be included in the workspace?"
+- options: List each child repo from `child_repos` array by name
+- multiSelect: true
+
+**If `child_repo_count` is 0 and `is_git_repo` is true:**
+
+Use AskUserQuestion:
+- header: "Current Repo"
+- question: "No child repos found. Create a workspace with the current repo?"
+- options:
+ - "Yes — create workspace with current repo" → use current repo
+ - "Cancel" → exit
+
+**If `child_repo_count` is 0 and `is_git_repo` is false:**
+
+Error:
+```
+No git repos found in the current directory and this is not a git repo.
+
+Run this command from a directory containing git repos, or specify repos explicitly:
+ /gsd:new-workspace --name my-workspace --repos /path/to/repo1,/path/to/repo2
+```
+Exit.
+
+**If `--auto` and `--repos` is NOT provided:**
+
+Error:
+```
+Error: --auto requires --repos to specify which repos to include.
+
+Usage:
+ /gsd:new-workspace --name my-workspace --repos repo1,repo2 --auto
+```
+Exit.
+
+## 4. Select Strategy
+
+**If `--strategy` is provided:** Use it (validate: must be `worktree` or `clone`).
+
+**If `--strategy` is NOT provided and not `--auto`:**
+
+Use AskUserQuestion:
+- header: "Strategy"
+- question: "How should repos be copied into the workspace?"
+- options:
+ - "Worktree (recommended) — lightweight, shares .git objects with source repo" → `worktree`
+ - "Clone — fully independent copy, no connection to source repo" → `clone`
+
+**If `--auto`:** Default to `worktree`.
+
+## 5. Validate
+
+Before creating anything, validate:
+
+1. **Target path** — must not exist or must be empty:
+```bash
+if [ -d "$TARGET_PATH" ] && [ "$(ls -A "$TARGET_PATH" 2>/dev/null)" ]; then
+ echo "Error: Target path already exists and is not empty: $TARGET_PATH"
+ echo "Choose a different --name or --path."
+ exit 1
+fi
+```
+
+2. **Source repos exist and are git repos** — for each repo path:
+```bash
+if [ ! -d "$REPO_PATH/.git" ]; then
+ echo "Error: Not a git repo: $REPO_PATH"
+ exit 1
+fi
+```
+
+3. **Worktree availability** — if strategy is `worktree` and `worktree_available` is false:
+```
+Error: git is not available. Install git or use --strategy clone.
+```
+
+Report all validation errors at once, not one at a time.
+
+## 6. Create Workspace
+
+```bash
+mkdir -p "$TARGET_PATH"
+```
+
+### For each repo:
+
+**Worktree strategy:**
+```bash
+cd "$SOURCE_REPO_PATH"
+git worktree add "$TARGET_PATH/$REPO_NAME" -b "$BRANCH_NAME" 2>&1
+```
+
+If `git worktree add` fails because the branch already exists, try with a timestamped branch:
+```bash
+TIMESTAMP=$(date +%Y%m%d%H%M%S)
+git worktree add "$TARGET_PATH/$REPO_NAME" -b "${BRANCH_NAME}-${TIMESTAMP}" 2>&1
+```
+
+If that also fails, report the error and continue with remaining repos.
+
+**Clone strategy:**
+```bash
+git clone "$SOURCE_REPO_PATH" "$TARGET_PATH/$REPO_NAME" 2>&1
+cd "$TARGET_PATH/$REPO_NAME"
+git checkout -b "$BRANCH_NAME" 2>&1
+```
+
+Track results: which repos succeeded, which failed, what branch was used.
+
+## 7. Write WORKSPACE.md
+
+Write the workspace manifest at `$TARGET_PATH/WORKSPACE.md`:
+
+```markdown
+# Workspace: $WORKSPACE_NAME
+
+Created: $DATE
+Strategy: $STRATEGY
+
+## Member Repos
+
+| Repo | Source | Branch | Strategy |
+|------|--------|--------|----------|
+| $REPO_NAME | $SOURCE_PATH | $BRANCH | $STRATEGY |
+...for each repo...
+
+## Notes
+
+[Add context about what this workspace is for]
+```
+
+## 8. Initialize .planning/
+
+```bash
+mkdir -p "$TARGET_PATH/.planning"
+```
+
+## 9. Report and Next Steps
+
+**If all repos succeeded:**
+
+```
+Workspace created: $TARGET_PATH
+
+ Repos: $REPO_COUNT
+ Strategy: $STRATEGY
+ Branch: $BRANCH_NAME
+
+Next steps:
+ cd $TARGET_PATH
+ /gsd:new-project # Initialize GSD in the workspace
+```
+
+**If some repos failed:**
+
+```
+Workspace created with $SUCCESS_COUNT of $TOTAL_COUNT repos: $TARGET_PATH
+
+ Succeeded: repo1, repo2
+ Failed: repo3 (branch already exists), repo4 (not a git repo)
+
+Next steps:
+ cd $TARGET_PATH
+ /gsd:new-project # Initialize GSD in the workspace
+```
+
+**Offer to initialize GSD (if not `--auto`):**
+
+Use AskUserQuestion:
+- header: "Initialize GSD"
+- question: "Would you like to initialize a GSD project in the new workspace?"
+- options:
+ - "Yes — run /gsd:new-project" → tell user to `cd $TARGET_PATH` first, then run `/gsd:new-project`
+ - "No — I'll set it up later" → done
+
+
+
+
+- [ ] Workspace directory created at target path
+- [ ] All specified repos copied (worktree or clone) into workspace
+- [ ] WORKSPACE.md manifest written with correct repo table
+- [ ] `.planning/` directory initialized at workspace root
+- [ ] User informed of workspace path and next steps
+
diff --git a/get-shit-done/workflows/remove-workspace.md b/get-shit-done/workflows/remove-workspace.md
new file mode 100644
index 000000000..321986744
--- /dev/null
+++ b/get-shit-done/workflows/remove-workspace.md
@@ -0,0 +1,90 @@
+
+Remove a GSD workspace, cleaning up git worktrees and deleting the workspace directory.
+
+
+
+Read all files referenced by the invoking prompt's execution_context before starting.
+
+
+
+
+## 1. Setup
+
+Extract workspace name from $ARGUMENTS.
+
+```bash
+INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init remove-workspace "$WORKSPACE_NAME")
+if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
+```
+
+Parse JSON for: `workspace_name`, `workspace_path`, `has_manifest`, `strategy`, `repos`, `repo_count`, `dirty_repos`, `has_dirty_repos`.
+
+**If no workspace name provided:**
+
+First run `/gsd:list-workspaces` to show available workspaces, then ask:
+
+Use AskUserQuestion:
+- header: "Remove Workspace"
+- question: "Which workspace do you want to remove?"
+- requireAnswer: true
+
+Re-run init with the provided name.
+
+## 2. Safety Checks
+
+**If `has_dirty_repos` is true:**
+
+```
+Cannot remove workspace "$WORKSPACE_NAME" — the following repos have uncommitted changes:
+
+ - repo1
+ - repo2
+
+Commit or stash changes in these repos before removing the workspace:
+ cd $WORKSPACE_PATH/repo1
+ git stash # or git commit
+```
+
+Exit. Do NOT proceed.
+
+## 3. Confirm Removal
+
+Use AskUserQuestion:
+- header: "Confirm Removal"
+- question: "Remove workspace '$WORKSPACE_NAME' at $WORKSPACE_PATH? This will delete all files in the workspace directory. Type the workspace name to confirm:"
+- requireAnswer: true
+
+**If answer does not match `$WORKSPACE_NAME`:** Exit with "Removal cancelled."
+
+## 4. Clean Up Worktrees
+
+**If strategy is `worktree`:**
+
+For each repo in the workspace:
+
+```bash
+cd "$SOURCE_REPO_PATH"
+git worktree remove "$WORKSPACE_PATH/$REPO_NAME" 2>&1 || true
+```
+
+If `git worktree remove` fails, warn but continue:
+```
+Warning: Could not remove worktree for $REPO_NAME — source repo may have been moved or deleted.
+```
+
+## 5. Delete Workspace Directory
+
+```bash
+rm -rf "$WORKSPACE_PATH"
+```
+
+## 6. Report
+
+```
+Workspace "$WORKSPACE_NAME" removed.
+
+ Path: $WORKSPACE_PATH (deleted)
+ Repos: $REPO_COUNT worktrees cleaned up
+```
+
+
diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs
index 06061caf7..297a94047 100644
--- a/tests/copilot-install.test.cjs
+++ b/tests/copilot-install.test.cjs
@@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => {
// Count gsd-* directories — should be 31
const dirs = fs.readdirSync(tempDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
- assert.strictEqual(dirs.length, 50, `expected 50 skill folders, got ${dirs.length}`);
+ assert.strictEqual(dirs.length, 53, `expected 53 skill folders, got ${dirs.length}`);
} finally {
fs.rmSync(tempDir, { recursive: true });
}
@@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process');
const crypto = require('crypto');
const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js');
-const EXPECTED_SKILLS = 50;
+const EXPECTED_SKILLS = 53;
const EXPECTED_AGENTS = 17;
function runCopilotInstall(cwd) {
diff --git a/tests/workspace.test.cjs b/tests/workspace.test.cjs
new file mode 100644
index 000000000..48ccfe572
--- /dev/null
+++ b/tests/workspace.test.cjs
@@ -0,0 +1,388 @@
+/**
+ * GSD Workspace Tests
+ *
+ * Tests for /gsd:new-workspace, /gsd:list-workspaces, /gsd:remove-workspace
+ * init functions and integration with gsd-tools routing.
+ */
+
+const { test, describe, beforeEach, afterEach } = require('node:test');
+const assert = require('node:assert');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { execSync } = require('child_process');
+const { runGsdTools, cleanup } = require('./helpers.cjs');
+const { detectChildRepos } = require('../get-shit-done/bin/lib/init.cjs');
+
+// ─── detectChildRepos ────────────────────────────────────────────────────────
+
+describe('detectChildRepos', () => {
+ let tmpDir;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-test-'));
+ });
+
+ afterEach(() => {
+ cleanup(tmpDir);
+ });
+
+ test('detects child git repos', () => {
+ // Create two child git repos
+ const repo1 = path.join(tmpDir, 'repo-a');
+ const repo2 = path.join(tmpDir, 'repo-b');
+ fs.mkdirSync(repo1);
+ fs.mkdirSync(repo2);
+ execSync('git init', { cwd: repo1, stdio: 'pipe' });
+ execSync('git init', { cwd: repo2, stdio: 'pipe' });
+
+ const repos = detectChildRepos(tmpDir);
+ assert.strictEqual(repos.length, 2);
+ const names = repos.map(r => r.name).sort();
+ assert.deepStrictEqual(names, ['repo-a', 'repo-b']);
+ });
+
+ test('skips non-git directories', () => {
+ const gitRepo = path.join(tmpDir, 'real-repo');
+ const notRepo = path.join(tmpDir, 'just-a-dir');
+ fs.mkdirSync(gitRepo);
+ fs.mkdirSync(notRepo);
+ execSync('git init', { cwd: gitRepo, stdio: 'pipe' });
+
+ const repos = detectChildRepos(tmpDir);
+ assert.strictEqual(repos.length, 1);
+ assert.strictEqual(repos[0].name, 'real-repo');
+ });
+
+ test('skips hidden directories', () => {
+ const hiddenRepo = path.join(tmpDir, '.hidden-repo');
+ fs.mkdirSync(hiddenRepo);
+ execSync('git init', { cwd: hiddenRepo, stdio: 'pipe' });
+
+ const repos = detectChildRepos(tmpDir);
+ assert.strictEqual(repos.length, 0);
+ });
+
+ test('skips files', () => {
+ fs.writeFileSync(path.join(tmpDir, 'some-file.txt'), 'hello');
+ const repos = detectChildRepos(tmpDir);
+ assert.strictEqual(repos.length, 0);
+ });
+
+ test('returns empty array for non-existent directory', () => {
+ const repos = detectChildRepos(path.join(tmpDir, 'does-not-exist'));
+ assert.strictEqual(repos.length, 0);
+ });
+});
+
+// ─── cmdInitNewWorkspace via gsd-tools ──────────────────────────────────────
+
+describe('init new-workspace', () => {
+ let tmpDir;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-test-'));
+ });
+
+ afterEach(() => {
+ cleanup(tmpDir);
+ });
+
+ test('returns expected JSON fields', () => {
+ const result = runGsdTools('init new-workspace', tmpDir);
+ assert.ok(result.success, `init failed: ${result.error}`);
+ const data = JSON.parse(result.output);
+ assert.ok('default_workspace_base' in data);
+ assert.ok('child_repos' in data);
+ assert.ok('child_repo_count' in data);
+ assert.ok('worktree_available' in data);
+ assert.ok('is_git_repo' in data);
+ assert.ok('cwd_repo_name' in data);
+ assert.ok('project_root' in data);
+ });
+
+ test('detects child git repos in cwd', () => {
+ const repo = path.join(tmpDir, 'my-repo');
+ fs.mkdirSync(repo);
+ execSync('git init', { cwd: repo, stdio: 'pipe' });
+
+ const result = runGsdTools('init new-workspace', tmpDir);
+ const data = JSON.parse(result.output);
+ assert.strictEqual(data.child_repo_count, 1);
+ assert.strictEqual(data.child_repos[0].name, 'my-repo');
+ });
+
+ test('reports no git repo when cwd is not a git repo', () => {
+ const result = runGsdTools('init new-workspace', tmpDir);
+ const data = JSON.parse(result.output);
+ assert.strictEqual(data.is_git_repo, false);
+ });
+});
+
+// ─── cmdInitListWorkspaces via gsd-tools ────────────────────────────────────
+
+describe('init list-workspaces', () => {
+ let tmpDir;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-test-'));
+ });
+
+ afterEach(() => {
+ cleanup(tmpDir);
+ });
+
+ test('returns empty list when no workspaces exist', () => {
+ const result = runGsdTools('init list-workspaces', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init failed: ${result.error}`);
+ const data = JSON.parse(result.output);
+ assert.strictEqual(data.workspace_count, 0);
+ assert.deepStrictEqual(data.workspaces, []);
+ });
+
+ test('finds workspaces with WORKSPACE.md', () => {
+ const wsBase = path.join(tmpDir, 'gsd-workspaces');
+ const ws1 = path.join(wsBase, 'feature-a');
+ fs.mkdirSync(path.join(ws1, '.planning'), { recursive: true });
+ fs.writeFileSync(path.join(ws1, 'WORKSPACE.md'), [
+ '# Workspace: feature-a',
+ '',
+ 'Created: 2026-03-20',
+ 'Strategy: worktree',
+ '',
+ '## Member Repos',
+ '',
+ '| Repo | Source | Branch | Strategy |',
+ '|------|--------|--------|----------|',
+ '| hr-ui | /tmp/hr-ui | workspace/feature-a | worktree |',
+ ].join('\n'));
+
+ const result = runGsdTools('init list-workspaces', tmpDir, { HOME: tmpDir });
+ const data = JSON.parse(result.output);
+ assert.strictEqual(data.workspace_count, 1);
+ assert.strictEqual(data.workspaces[0].name, 'feature-a');
+ assert.strictEqual(data.workspaces[0].strategy, 'worktree');
+ assert.strictEqual(data.workspaces[0].repo_count, 1);
+ });
+});
+
+// ─── cmdInitRemoveWorkspace via gsd-tools ───────────────────────────────────
+
+describe('init remove-workspace', () => {
+ let tmpDir;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-test-'));
+ });
+
+ afterEach(() => {
+ cleanup(tmpDir);
+ });
+
+ test('errors when no name provided', () => {
+ const result = runGsdTools('init remove-workspace', tmpDir);
+ assert.strictEqual(result.success, false);
+ assert.ok(result.error.includes('workspace name required'));
+ });
+
+ test('errors when workspace not found', () => {
+ const result = runGsdTools('init remove-workspace nonexistent', tmpDir, { HOME: tmpDir });
+ assert.strictEqual(result.success, false);
+ assert.ok(result.error.includes('Workspace not found'));
+ });
+
+ test('returns workspace info for existing workspace', () => {
+ const wsBase = path.join(tmpDir, 'gsd-workspaces');
+ const ws = path.join(wsBase, 'test-ws');
+ fs.mkdirSync(ws, { recursive: true });
+ fs.writeFileSync(path.join(ws, 'WORKSPACE.md'), [
+ '# Workspace: test-ws',
+ '',
+ 'Created: 2026-03-20',
+ 'Strategy: clone',
+ '',
+ '## Member Repos',
+ '',
+ '| Repo | Source | Branch | Strategy |',
+ '|------|--------|--------|----------|',
+ '| api | /tmp/api | workspace/test-ws | clone |',
+ ].join('\n'));
+
+ const result = runGsdTools('init remove-workspace test-ws', tmpDir, { HOME: tmpDir });
+ assert.ok(result.success, `init failed: ${result.error}`);
+ const data = JSON.parse(result.output);
+ assert.strictEqual(data.workspace_name, 'test-ws');
+ assert.strictEqual(data.strategy, 'clone');
+ assert.strictEqual(data.has_dirty_repos, false);
+ });
+});
+
+// ─── Integration: worktree creation and removal ─────────────────────────────
+
+describe('workspace worktree integration', () => {
+ let tmpDir;
+ let sourceRepo;
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-integ-'));
+ // Create a source git repo with a commit
+ sourceRepo = path.join(tmpDir, 'source-repo');
+ fs.mkdirSync(sourceRepo);
+ execSync('git init', { cwd: sourceRepo, stdio: 'pipe' });
+ execSync('git config user.email "test@test.com"', { cwd: sourceRepo, stdio: 'pipe' });
+ execSync('git config user.name "Test"', { cwd: sourceRepo, stdio: 'pipe' });
+ fs.writeFileSync(path.join(sourceRepo, 'README.md'), '# Test Repo\n');
+ execSync('git add -A', { cwd: sourceRepo, stdio: 'pipe' });
+ execSync('git commit -m "initial"', { cwd: sourceRepo, stdio: 'pipe' });
+ });
+
+ afterEach(() => {
+ // Clean up worktrees before removing tmp dir
+ try {
+ execSync('git worktree prune', { cwd: sourceRepo, stdio: 'pipe' });
+ } catch { /* best-effort */ }
+ cleanup(tmpDir);
+ });
+
+ test('creates workspace with git worktree', () => {
+ const wsPath = path.join(tmpDir, 'my-workspace');
+ fs.mkdirSync(wsPath);
+ fs.mkdirSync(path.join(wsPath, '.planning'));
+
+ // Create worktree
+ execSync(`git worktree add "${path.join(wsPath, 'source-repo')}" -b workspace/test`, {
+ cwd: sourceRepo,
+ stdio: 'pipe',
+ });
+
+ // Verify worktree was created
+ assert.ok(fs.existsSync(path.join(wsPath, 'source-repo', 'README.md')));
+
+ // Verify it's a worktree (has .git file, not .git directory)
+ const gitPath = path.join(wsPath, 'source-repo', '.git');
+ assert.ok(fs.existsSync(gitPath));
+ const stat = fs.statSync(gitPath);
+ assert.ok(stat.isFile(), '.git should be a file (worktree link), not a directory');
+ });
+
+ test('creates workspace with git clone', () => {
+ const wsPath = path.join(tmpDir, 'cloned-workspace');
+ fs.mkdirSync(wsPath);
+
+ // Clone repo
+ execSync(`git clone "${sourceRepo}" "${path.join(wsPath, 'source-repo')}"`, {
+ stdio: 'pipe',
+ });
+
+ // Verify clone
+ assert.ok(fs.existsSync(path.join(wsPath, 'source-repo', 'README.md')));
+
+ // Verify it's a full clone (has .git directory)
+ const gitPath = path.join(wsPath, 'source-repo', '.git');
+ const stat = fs.statSync(gitPath);
+ assert.ok(stat.isDirectory(), '.git should be a directory (full clone)');
+ });
+
+ test('worktree removal cleans up properly', () => {
+ const wsPath = path.join(tmpDir, 'removable-ws');
+ fs.mkdirSync(wsPath);
+
+ // Create worktree
+ execSync(`git worktree add "${path.join(wsPath, 'source-repo')}" -b workspace/removable`, {
+ cwd: sourceRepo,
+ stdio: 'pipe',
+ });
+
+ assert.ok(fs.existsSync(path.join(wsPath, 'source-repo', 'README.md')));
+
+ // Remove worktree
+ execSync(`git worktree remove "${path.join(wsPath, 'source-repo')}"`, {
+ cwd: sourceRepo,
+ stdio: 'pipe',
+ });
+
+ // Verify worktree is gone
+ assert.ok(!fs.existsSync(path.join(wsPath, 'source-repo')));
+
+ // Verify worktree list doesn't include it
+ const worktrees = execSync('git worktree list', { cwd: sourceRepo, encoding: 'utf8' });
+ assert.ok(!worktrees.includes('removable-ws'));
+ });
+});
+
+// ─── Command and workflow file existence ────────────────────────────────────
+
+describe('workspace command files', () => {
+ const baseDir = path.join(__dirname, '..');
+
+ test('new-workspace command exists with correct frontmatter', () => {
+ const content = fs.readFileSync(path.join(baseDir, 'commands/gsd/new-workspace.md'), 'utf8');
+ assert.ok(content.includes('name: gsd:new-workspace'));
+ assert.ok(content.includes('--name'));
+ assert.ok(content.includes('--repos'));
+ assert.ok(content.includes('--strategy'));
+ assert.ok(content.includes('workflows/new-workspace.md'));
+ });
+
+ test('list-workspaces command exists with correct frontmatter', () => {
+ const content = fs.readFileSync(path.join(baseDir, 'commands/gsd/list-workspaces.md'), 'utf8');
+ assert.ok(content.includes('name: gsd:list-workspaces'));
+ assert.ok(content.includes('workflows/list-workspaces.md'));
+ });
+
+ test('remove-workspace command exists with correct frontmatter', () => {
+ const content = fs.readFileSync(path.join(baseDir, 'commands/gsd/remove-workspace.md'), 'utf8');
+ assert.ok(content.includes('name: gsd:remove-workspace'));
+ assert.ok(content.includes('workflows/remove-workspace.md'));
+ });
+
+ test('new-workspace workflow exists', () => {
+ const content = fs.readFileSync(path.join(baseDir, 'get-shit-done/workflows/new-workspace.md'), 'utf8');
+ assert.ok(content.includes('init new-workspace'));
+ assert.ok(content.includes('WORKSPACE.md'));
+ assert.ok(content.includes('git worktree add'));
+ assert.ok(content.includes('git clone'));
+ });
+
+ test('list-workspaces workflow exists', () => {
+ const content = fs.readFileSync(path.join(baseDir, 'get-shit-done/workflows/list-workspaces.md'), 'utf8');
+ assert.ok(content.includes('init list-workspaces'));
+ });
+
+ test('remove-workspace workflow exists', () => {
+ const content = fs.readFileSync(path.join(baseDir, 'get-shit-done/workflows/remove-workspace.md'), 'utf8');
+ assert.ok(content.includes('init remove-workspace'));
+ assert.ok(content.includes('git worktree remove'));
+ });
+});
+
+// ─── Routing in gsd-tools ───────────────────────────────────────────────────
+
+describe('workspace routing in gsd-tools', () => {
+ test('init new-workspace is routed correctly', () => {
+ const toolsContent = fs.readFileSync(
+ path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
+ 'utf8'
+ );
+ assert.ok(toolsContent.includes("case 'new-workspace'"));
+ assert.ok(toolsContent.includes('cmdInitNewWorkspace'));
+ });
+
+ test('init list-workspaces is routed correctly', () => {
+ const toolsContent = fs.readFileSync(
+ path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
+ 'utf8'
+ );
+ assert.ok(toolsContent.includes("case 'list-workspaces'"));
+ assert.ok(toolsContent.includes('cmdInitListWorkspaces'));
+ });
+
+ test('init remove-workspace is routed correctly', () => {
+ const toolsContent = fs.readFileSync(
+ path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
+ 'utf8'
+ );
+ assert.ok(toolsContent.includes("case 'remove-workspace'"));
+ assert.ok(toolsContent.includes('cmdInitRemoveWorkspace'));
+ });
+});