gsd-core 的 /gsd:workspace 命令详解:创建、列出与清理隔离的 Git 工作区

原创2026-10-08 15:17:27376 阅读

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 步)要求"一次性报告所有错误,而不是逐个报错":

  1. 目标路径必须不存在或为空目录(否则 Error: Target path already exists and is not empty);
  2. 每个源仓库必须含 .git(否则 Error: Not a git repo: $REPO_PATH);
  3. 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'));

三个实现事实:

  1. 目录必须含 WORKSPACE.md 才被识别为工作区(if (!fs.existsSync(manifestPath)) continue;)——这就是创建流程坚持写清单的原因;
  2. Strategy 用正则 ^Strategy:\s*(.+)$(多行模式)从清单头部提取;
  3. 仓库数通过统计清单中的 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> 查询后,走一条严格的安全链:

  1. 无名称:先执行 /gsd:workspace --list 展示可用工作区,再询问要移除哪个,然后带着名称重新执行 init;
  2. 脏仓库拦截:若 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
  1. 确认移除:用 AskUserQuestion 要求键入工作区名称作为确认(requireAnswer: true),答案不匹配 WORKSPACE_NAME 则以 "Removal cancelled." 退出;
  2. 清理 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";
  3. 删除目录:仅在 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 三个模式(创建、列出、移除)所共同维护的隔离契约。

参考文件

登录后查看全文
gsd-core