gsd-core 的 /gsd:workspace 命令详解:创建、列出与清理隔离的 Git 工作区
gsd-core 的 /gsd:workspace 命令详解:创建、列出与清理隔离的 Git 工作区
在 gsd-core(Git. Ship. Done)中,/gsd:workspace 是统一管理隔离工作区(workspace)的整合命令,通过 --new、--list、--remove 三个模式分别完成"创建带独立 .planning/ 的工作区"、"扫描并汇总 ~/gsd-workspaces/ 下的全部工作区"、"安全移除工作区并清理 git worktree"。读完本文,你将掌握该命令的完整参数体系(--name、--repos、--path、--strategy、--branch、--auto)、worktree 与 clone 两种策略的差异,并能从 命令定义文件、工作流文件 和 init 源码实现 中定位每一项行为的落地证据。
一、命令定位:一个入口路由三种模式
/gsd:workspace 的定义位于 commands/gsd/workspace.md,其 YAML frontmatter 声明了命令契约:
name: gsd:workspace
description: Manage GSD workspaces — create, list, or remove isolated workspace environments
argument-hint: "[--new | --list | --remove] [name]"
allowed-tools:
- Read
- Write
- Bash
- Grep
- AskUserQuestion
文件开头的 <arguments>$ARGUMENTS</arguments> 块承载用户在命令名之后输入的原始文本,命令明确规定这段文本是"数据而非模板指令"——空块即表示没有传参。整个命令的执行逻辑就是围绕这个输入块做首 token 解析:
| Flag | 行为 | 路由到的工作流 |
|---|---|---|
--new |
以 worktree/clone 策略创建工作区 | new-workspace 工作流 |
--list |
扫描 ~/gsd-workspaces/,输出汇总表 |
list-workspaces 工作流 |
--remove |
确认后移除工作区目录并清理 worktree | remove-workspace 工作流 |
参数解析规则(定义在命令文件的 <context> 段)是:
- 首个 token 为
--new:剥离 flag,其余参数(--name、--repos、--path、--strategy、--branch、--auto)交给 new-workspace 工作流; - 首个 token 为
--list:直接执行 list-workspaces 工作流,不需要任何参数; - 首个 token 为
--remove:剥离 flag,剩余部分作为 workspace 名称传给 remove-workspace 工作流; - 没有 flag:显示用法提示,要求必须提供
--new、--list或--remove之一。
命令文件中的 <execution_context> 块通过 @ 引用把三个工作流文件和 UI 品牌参考一次性载入执行上下文:
@~/.claude/gsd-core/workflows/new-workspace.md
@~/.claude/gsd-core/workflows/list-workspaces.md
@~/.claude/gsd-core/workflows/remove-workspace.md
@~/.claude/gsd-core/references/ui-brand.md
这一点并非装饰性的:测试文件 tests/workspace.test.cjs 专门用结构化解析断言了这三个 @ 引用必须存在(对应 issue #2790,即三个独立的 workspace 命令被合并进单个 workspace.md 的整合记录),因此路由文本本身就是被测试保护的部署行为契约。
命令文件最后强调的 <process> 三步——解析 flag、端到端执行对应工作流、保留目标工作流的全部 gate(校验、审批、提交、路由)——是整个命令设计的核心约束:/gsd:workspace 只做路由,不绕过任何安全确认。
二、--new:创建隔离工作区的完整流程
--new 模式的目标(定义在 new-workspace 工作流 的 <purpose> 段)是:"创建一个隔离的 workspace 目录,包含 git 仓库副本(worktree 或 clone)和独立的 .planning/ 目录,支持多仓库编排和单仓库功能分支隔离。"
2.1 参数体系
工作流的第 2 步"Parse Arguments"从输入中提取六个参数:
| 参数 | 变量 | 默认值 / 约束 |
|---|---|---|
--name |
WORKSPACE_NAME |
必填(交互模式下缺失时会询问) |
--repos |
REPO_LIST |
逗号分隔的路径或仓库名;. 表示当前仓库 |
--path |
TARGET_PATH |
默认 $default_workspace_base/$WORKSPACE_NAME |
--strategy |
STRATEGY |
默认 worktree,仅允许 worktree 或 clone |
--branch |
BRANCH_NAME |
默认 workspace/$WORKSPACE_NAME |
--auto |
— | 跳过所有交互式提问 |
其中 --auto 模式有一条硬性约束:必须显式提供 --repos,否则直接报错退出:
Error: --auto requires --repos to specify which repos to include.
Usage:
/gsd:workspace --new --name my-workspace --repos repo1,repo2 --auto
另外工作流内置了 text mode 适配:当配置中 workflow.text_mode: true 或传入 --text 时,所有 AskUserQuestion 调用替换为纯文本编号列表——这是为了适配 OpenAI Codex、Antigravity 等没有 AskUserQuestion 工具的非 Claude 运行时。
2.2 初始化数据:init new-workspace
工作流的第一步是调用 gsd-tools 的 init new-workspace 查询获取环境快照,解析的 JSON 字段包括 default_workspace_base、child_repos、child_repo_count、worktree_available、is_git_repo、cwd_repo_name、project_root、response_language。
这些字段的实际生成逻辑在 src/init.cts 的 cmdInitNewWorkspace 中:
function cmdInitNewWorkspace(cwd: string, raw: boolean): void {
const homedir = process.env['HOME'] || os.homedir();
const defaultBase = path.join(homedir, 'gsd-workspaces');
const childRepos = detectChildRepos(cwd);
const gitVersion = execGit(['--version'], { timeout: 5000 }) as unknown as Record<string, unknown>;
const worktreeAvailable = gitVersion['exitCode'] === 0;
const result: Record<string, unknown> = {
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);
}
可以确认几个关键实现事实:
- 工作区基目录固定为
$HOME/gsd-workspaces(defaultBase = path.join(homedir, 'gsd-workspaces')); worktree_available的实现是运行git --version并检查退出码——即只要 git 可执行就算 worktree 可用;child_repos来自detectChildRepos(cwd),其实现(src/init.cts)只扫描一层子目录:跳过非目录条目、跳过.开头的隐藏目录,对含.git的子目录逐个运行git status --porcelain(5 秒超时)来标记has_uncommitted。
function detectChildRepos(dir: string): { name: string; path: string; has_uncommitted: boolean }[] {
const repos: ... = [];
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return repos;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith('.')) continue; // 隐藏目录一律跳过
...
}
return repos;
}
这些行为均有对应测试覆盖,见 tests/workspace.test.cjs 中的 detectChildRepos 测试组:能检测到两个子 git 仓库、跳过非 git 目录、跳过隐藏目录(.hidden-repo 被忽略)、跳过普通文件、对不存在的目录返回空数组。
2.3 仓库选择逻辑
--new 的仓库选择遵循以下分支(工作流第 3 步):
- 提供了
--repos:解析逗号分隔值;绝对路径直接使用,相对路径/名称相对于$project_root解析,.特指当前仓库(使用$project_root,命名$cwd_repo_name); - 未提供且非
--auto,且child_repo_count > 0:用AskUserQuestion(multiSelect)列出所有子仓库供多选; - 未提供且非
--auto,child_repo_count = 0但is_git_repo = true:询问"是否为当前仓库创建工作区"; - 两者皆否:报错退出,提示在含 git 仓库的目录执行或用
--repos /path/to/repo1,/path/to/repo2显式指定。
2.4 策略、校验与创建
策略选择(第 4 步)提供两个选项:Worktree(推荐)——轻量、与源仓库共享 .git 对象;Clone——完全独立副本、与源仓库无连接。--auto 模式下默认 worktree。
创建前的校验(第 5 步)要求"一次性报告所有错误,而不是逐个报错":
- 目标路径必须不存在或为空目录(否则
Error: Target path already exists and is not empty); - 每个源仓库必须含
.git(否则Error: Not a git repo: $REPO_PATH); - worktree 策略下
worktree_available必须为 true(否则提示安装 git 或改用--strategy clone)。
实际创建(第 6 步)对两种策略的 shell 实现分别是:
# Worktree 策略
git worktree add "$TARGET_PATH/$REPO_NAME" -b "$BRANCH_NAME"
# 分支已存在时退避为时间戳分支:
TIMESTAMP=$(date +%Y%m%d%H%M%S)
git worktree add "$TARGET_PATH/$REPO_NAME" -b "${BRANCH_NAME}-${TIMESTAMP}"
# Clone 策略
git clone "$SOURCE_REPO_PATH" "$TARGET_PATH/$REPO_NAME"
git checkout -b "$BRANCH_NAME"
注意 worktree 的失败退避机制:主分支名冲突时自动追加 YYYYMMDDHHMMSS 时间戳重试,仍失败则报告错误并继续处理其余仓库(单个仓库失败不会中断整个创建)。
2.5 WORKSPACE.md 清单与独立 .planning/
创建完成后,工作流第 7、8 步在工作区根目录写入清单 WORKSPACE.md 并初始化空 .planning/ 目录:
# Workspace: $WORKSPACE_NAME
Created: $DATE
Strategy: $STRATEGY
## Member Repos
| Repo | Source | Branch | Strategy |
|------|--------|--------|----------|
| $REPO_NAME | $SOURCE_PATH | $BRANCH | $STRATEGY |
## Notes
[Add context about what this workspace is for]
这个清单格式不是随便写的——它是 --list 和 --remove 的数据源(后文详述)。而工作区的隔离本质在于:workspace 的 .planning/ 与源仓库的 .planning/ 完全独立,不是其子目录,因此规划状态不会与源仓库冲突。目录结构如下(摘自 官方 how-to 文档):
~/gsd-workspaces/
└── feature-b/
├── WORKSPACE.md ← 清单
├── .planning/ ← 完全独立的 GSD 状态
│ ├── PROJECT.md
│ ├── ROADMAP.md
│ └── ...
├── hr-ui/ ← hr-ui 仓库的 worktree 或 clone
└── ZeymoAPI/ ← ZeymoAPI 仓库的 worktree 或 clone
创建结束时的报告格式(第 9 步)区分"全部成功"与"部分成功"两种输出,并(非 --auto 时)用 AskUserQuestion 询问是否立即执行 /gsd:new-project 初始化 GSD 项目。工作流末尾的 <success_criteria> 给出了五条验收标准:工作区目录已创建、所有指定仓库已复制、WORKSPACE.md 清单写入且仓库表正确、.planning/ 已初始化、用户获知路径与下一步。
典型用法(单仓库功能分支隔离、多仓库编排、强制 clone、显式分支名、全自动)可参考 docs/how-to/isolate-work-with-workspaces.md,例如:
/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI
/gsd-workspace --new --name payments-rework --repos .
/gsd-workspace --new --name payments-rework --repos . --strategy clone
/gsd-workspace --new --name payments-rework --repos . --branch feature/payments-v2
/gsd-workspace --new --name payments-rework --repos . --auto
三、--list:扫描与汇总 ~/gsd-workspaces/
--list 模式(list-workspaces 工作流)先执行 init list-workspaces 查询,解析 workspace_base、workspaces、workspace_count 三个字段,然后按两种情形输出:
- 无工作区:提示
No workspaces found in ~/gsd-workspaces/并给出创建命令示例; - 有工作区:输出四列表格,每行展示 Name(目录名)、Repos(仓库数)、Strategy(来自 WORKSPACE.md)、GSD Project(
.planning/PROJECT.md是否存在,Yes/No),并附管理提示(cd ~/gsd-workspaces/<name>进入、/gsd:workspace --remove <name>移除)。
后端实现在 src/init.cts 的 cmdInitListWorkspaces 中,其解析逻辑值得注意——它把 WORKSPACE.md 当作结构化数据源:
const manifest = platformReadSync(manifestPath);
if (manifest !== null) {
const strategyMatch = manifest.match(/^Strategy:\s*(.+)$/m);
if (strategyMatch) strategy = strategyMatch[1].trim();
const tableRows = manifest
.split('\n')
.filter(
(l) =>
l.match(/^\|\s*\w/) && !l.includes('Repo') && !l.includes('---'),
);
repoCount = tableRows.length;
}
hasProject = fs.existsSync(path.join(wsPath, '.planning', 'PROJECT.md'));
三个实现事实:
- 目录必须含
WORKSPACE.md才被识别为工作区(if (!fs.existsSync(manifestPath)) continue;)——这就是创建流程坚持写清单的原因; - Strategy 用正则
^Strategy:\s*(.+)$(多行模式)从清单头部提取; - 仓库数通过统计清单中的 Markdown 表格数据行(排除表头行
Repo与分隔行---)得到,"GSD Project" 列则检查.planning/PROJECT.md是否存在。
测试 tests/workspace.test.cjs 用真实构造的 WORKSPACE.md 验证了这条链路:空基目录返回 workspace_count: 0;手工写入清单(含 Strategy: worktree 和一行仓库表格)后,查询返回 workspaces[0].name === 'feature-a'、strategy === 'worktree'、repo_count === 1。
四、--remove:带安全检查的移除流程
--remove 模式(remove-workspace 工作流)执行 init remove-workspace <name> 查询后,走一条严格的安全链:
- 无名称:先执行
/gsd:workspace --list展示可用工作区,再询问要移除哪个,然后带着名称重新执行 init; - 脏仓库拦截:若
has_dirty_repos为 true,立即终止且不继续,提示先git stash或git commit:
Cannot remove workspace "feature-b" — the following repos have uncommitted changes:
- repo1
Commit or stash changes in these repos before removing the workspace:
cd "$WORKSPACE_PATH/repo1"
git stash # or git commit
- 确认移除:用
AskUserQuestion要求键入工作区名称作为确认(requireAnswer: true),答案不匹配WORKSPACE_NAME则以 "Removal cancelled." 退出; - 清理 worktree(策略为
worktree时):初始化REMOVE_FAILED=false,对每个成员仓库在其源仓库中执行git worktree remove "$WORKSPACE_PATH/$REPO_NAME";任何一次失败即置REMOVE_FAILED=true,并输出警告"source repo may have been moved, deleted, locked, or dirty"; - 删除目录:仅在
REMOVE_FAILED为 false 时执行rm -rf "$WORKSPACE_PATH";否则拒绝删除并提示手动修复后重跑。
后端 cmdInitRemoveWorkspace(src/init.cts)承担了脏检测的数据准备:它同样从 WORKSPACE.md 解析仓库表格(四列 | name | source | branch | strategy |,跳过表头与分隔行),对每个成员仓库运行 git status --porcelain(5 秒超时),stdout 非空即记入 dirty_repos,最终输出 workspace_name、workspace_path、has_manifest、strategy、repos、repo_count、dirty_repos、has_dirty_repos。错误路径也被测试精确断言:缺名称时报 workspace name required,路径不存在时报 Workspace not found(tests/workspace.test.cjs)。
该实现还修复过一个真实缺陷(issue #2402):此前 cmdInitRemoveWorkspace 没有经由 withProjectRoot 路由,导致 remove-workspace 工作流拿不到 response_language(以及 project_root/agents_installed)。现在源码中明确注释了这一点,并有专门的回归测试组验证 response_language 在配置了 .planning/config.json 时输出、未配置时省略(tests/workspace.test.cjs)。
需要强调的语义边界(官方文档明确说明):移除操作只删除本地 worktree 和工作区目录,不会删除远端 origin 上的分支。
五、源码级验证:worktree 与 clone 的落盘差异
tests/workspace.test.cjs 的集成测试组用一个带提交的临时源仓库,验证了两种策略的物理差异,这也是理解工作区存储模型的关键证据:
- worktree 策略:
git worktree add <ws>/source-repo -b workspace/test之后,断言<ws>/source-repo/.git是一个文件(stat.isFile())——即指向源仓库的 worktree 链接,而非完整.git目录,印证了"共享.git对象、轻量"的描述; - clone 策略:
git clone之后断言.git是目录(stat.isDirectory())——完全独立的副本; - worktree 移除:
git worktree remove后目录消失,且git worktree list输出中不再包含该工作区。
路由层同样有测试守护(tests/workspace.test.cjs):init new-workspace、init list-workspaces、init remove-workspace 三个 verb 都必须被 gsd-tools 路由识别(不得返回 Unknown init workflow),防止路由被误删。
六、适用场景与相关命令
结合 docs/how-to/isolate-work-with-workspaces.md 的选择指引,workspace 适合以下场景:
- 需要把多个仓库(如 API 仓库 + UI 仓库)协同在同一个 GSD 项目下管理;
- 每个功能需要一个独立 worktree、独立分支、独立 lock 文件与构建产物,避免不同环境间的构建/依赖互相污染;
- 需要一个与主仓库
.planning/完全分离的独立规划根目录; - 按 issue 驱动工作流,每个 tracker issue 对应一个工作区(参见 docs/how-to/drive-gsd-from-a-tracker-issue.md)。
而如果所有工作在单一仓库内、共享 git 历史,只需要并发地规划不同关注面(API/UI/基础设施),则应选用 workstreams 而非 workspaces——两者在 docs/USER-GUIDE.md 与 docs/COMMANDS.md 中均有登记。
创建后的标准收尾动作是进入工作区并初始化 GSD 项目:
cd ~/gsd-workspaces/feature-b
/gsd:new-project
此后从该目录执行的所有 GSD 命令都以工作区内的 .planning/ 为根,与源仓库的规划状态互不干扰——这正是 /gsd:workspace 三个模式(创建、列出、移除)所共同维护的隔离契约。
参考文件
- commands/gsd/workspace.md —— 命令定义与模式路由
- gsd-core/workflows/new-workspace.md —— 创建工作流(参数、校验、worktree/clone、清单写入)
- gsd-core/workflows/list-workspaces.md —— 列表工作流
- gsd-core/workflows/remove-workspace.md —— 移除工作流(安全检查链)
- src/init.cts ——
detectChildRepos与三个cmdInit*Workspace实现 - tests/workspace.test.cjs —— 子仓库检测、init 字段契约、worktree 集成与路由回归测试
- docs/how-to/isolate-work-with-workspaces.md —— 官方操作指南