Back to Qwen Code

Worktree 通用能力设计

docs/design/worktree.md

0.16.226.0 KB
Original Source

Worktree 通用能力设计

问题陈述

qwen-code 目前仅有面向 Arena 多模型对比场景的内部 worktree 实现(GitWorktreeService),用户无法在普通会话中使用 worktree 隔离工作,AgentTool 也不支持为 subagent 创建隔离的 worktree 环境。

目标是将 worktree 做成通用能力,支持用户会话级隔离和 Agent 级隔离,同时保证现有 Arena 功能体验完全不变。

现状对比

功能qwen-codeclaude-code阶段
EnterWorktree 工具✅(Phase A)
ExitWorktree 工具✅(Phase A)
AgentTool isolation: 'worktree'✅(Phase B)
过期 worktree 自动清理✅(Phase B)
worktree 会话状态持久化与恢复Phase C
Post-creation setup(hooks 配置)Phase C
StatusLine worktree 状态展示Phase C
WorktreeExitDialog(退出提示)Phase C
--worktree CLI 启动标志✅(Phase D)
符号链接目录(node_modules 等)✅(Phase D)
PR 引用(--worktree=#123✅(Phase D)
sparse checkoutFuture
tmux 集成Future
Arena 多模型 worktree 隔离✅(qwen 独有)
脏状态覆盖(stash + copy)
Baseline commit 追踪✅(qwen 独有)

设计原则

worktree 是通用能力,Arena 是其上层应用。

  • 通用 worktree 层:EnterWorktree/ExitWorktree 工具、AgentTool isolation 参数、会话状态管理、自动清理
  • Arena 层:多模型并行调度、worktreeBaseDir 自定义路径、批量创建与 diff 对比,继续使用 GitWorktreeService.setupWorktrees() 的现有逻辑,不受通用层改动影响

AgentTool 的 isolation: 'worktree' 只走通用路径,Arena 内部不经过这个参数创建 worktree,两者路径独立。

路径与配置

通用 worktree 路径

EnterWorktree 工具或 AgentTool isolation: 'worktree' 创建的 worktree 固定存放在:

{git 仓库根}/.qwen/worktrees/{slug}

路径不可配置。slug 命名规则:

  • 用户会话 worktree:用户指定名称,或自动生成(格式:{形容词}-{名词}-{4位随机}
  • Agent worktree:agent-{7位随机 hex}

Arena worktree 路径(已有,保持不变)

Arena 的 worktree 路径由 agents.arena.worktreeBaseDir 控制,默认 ~/.qwen/arenaArenaManager.ts:125),与通用路径完全独立,不做任何改动。

扩展配置

配置项类型用途阶段
ui.hideBuiltinWorktreeIndicatorboolean隐藏 Footer 中内置 ⎇ worktree-… (…) 行,留给 custom statuslinePhase C
worktree.symlinkDirectoriesstring[]符号链接指定目录(如 node_modules)到 worktree,避免磁盘浪费Phase D
worktree.sparsePathsstring[]git sparse-checkout cone 模式,大型 monorepo 只写入指定路径Future

Phase A / B 不新增任何配置项。

工具设计

EnterWorktree

触发条件: 用户明确说 "start a worktree"、"use a worktree"、"create a worktree" 等词语。不应在用户说"修复 bug"、"开发功能"时自动触发。

输入 schema:

name?: string  // 可选,slug 格式:字母/数字/点/下划线/破折号,最大 64 字符

行为:

  1. 验证当前未在 worktree 中(防止嵌套)
  2. 解析到 git 仓库根(处理已在子目录的情况)
  3. 调用 GitWorktreeService 创建 worktree,路径为 .qwen/worktrees/{slug}
  4. 将 worktree 会话写入 SessionService
  5. 切换工作目录到 worktree 路径
  6. 清除文件缓存

输出: worktreePathworktreeBranchmessage

ExitWorktree

触发条件: 用户说 "exit the worktree"、"leave the worktree"、"go back" 等。

输入 schema:

action: 'keep' | 'remove'
discard_changes?: boolean  // 仅 action='remove' 时有效

安全守卫:

  • 仅操作本会话通过 EnterWorktree 创建的 worktree
  • action='remove' 且存在未提交变更时,拒绝执行(除非 discard_changes: true

行为:

  • keep:清空会话中的 worktree 状态,保留 worktree 目录和分支,恢复原始工作目录
  • remove:删除 worktree 目录,删除对应 git 分支,清空会话状态,恢复原始工作目录

输出: actionoriginalCwdworktreePathworktreeBranch

用户触发方式

方式示例实现阶段
会话中明确请求用户说 "在 worktree 中开始工作" → 模型调用 EnterWorktreePhase A
Agent 隔离模型为 subagent 设置 isolation: 'worktree'Phase B
CLI 启动标志qwen --worktree my-featurePhase D

无斜杠命令。会话中 worktree 的触发依赖用户明确提及,isolation: 'worktree' 才是模型自主决策的场景。

分阶段实现计划

Phase A:核心工具(用户会话级 worktree)

目标: 用户能在会话中进入 / 退出 worktree。

要实现的功能:

  • EnterWorktree 工具:创建 worktree,切换工作目录,记录会话状态
  • ExitWorktree 工具:keep / remove 两种退出方式,安全守卫
  • GitWorktreeService 扩展:新增面向单用户会话的 createUserWorktree() / removeUserWorktree() 方法,复用现有 git 操作逻辑,不改动 Arena 使用的批量接口
  • SessionService 扩展:新增 WorktreeSession 字段,记录 { slug, worktreePath, worktreeBranch, originalCwd, originalBranch }--resume 时恢复 worktree 工作目录
  • 工具 prompt:为每个工具编写使用说明,明确何时调用、何时不调用

影响文件:

文件变更类型
packages/core/src/tools/tool-names.ts新增 ENTER_WORKTREEEXIT_WORKTREE 常量
packages/core/src/tools/EnterWorktreeTool/新建目录:EnterWorktreeTool.tsprompt.ts
packages/core/src/tools/ExitWorktreeTool/新建目录:ExitWorktreeTool.tsprompt.ts
packages/core/src/services/gitWorktreeService.ts新增用户会话级接口(不改动 Arena 接口)
packages/core/src/services/sessionService.ts新增 WorktreeSession 字段及读写方法
packages/core/src/tools/ 注册入口注册新工具

不在 Phase A 范围内:

  • Agent 隔离(Phase B)
  • hooks 配置等 post-creation setup(Phase C)
  • UI 状态展示(Phase C)

Phase B:Agent 隔离(AgentTool isolation: 'worktree')+ 描述更新

目标: 模型可为 subagent 创建临时隔离 worktree,agent 结束后自动清理;同步更新受影响的工具描述和提示词。

要实现的功能:

Agent 隔离核心:

  • AgentTool 新增 isolation?: 'worktree' 参数
  • Agent 启动时创建临时 worktree(slug:agent-{7hex},路径:.qwen/worktrees/agent-{7hex}
  • Agent 结束后:无变更则自动删除;有变更则保留,将路径和分支返回在结果中
  • 过期 worktree 自动清理:扫描 .qwen/worktrees/,匹配 agent-{7hex} 模式,超过 30 天且无未推送提交则删除,fail-closed 策略

描述与提示词更新:

  • AgentTool description 补充 isolation: 'worktree' 参数说明(参考 claude-code AgentTool/prompt.ts:272
  • 新增 buildWorktreeNotice():当 fork subagent 在 worktree 中运行时,向其注入上下文提示,说明其处于隔离 worktree、路径继承自父 agent、编辑前需重新读取文件(参考 claude-code forkSubagent.ts:buildWorktreeNotice

无需改动:

  • review skill(SKILL.md):review 使用独立机制(路径 .qwen/tmp/review-pr-<n>,通过 qwen review fetch-pr 命令创建),与通用 worktree 路径和机制完全不同,不存在混淆

Arena 兼容保证: Arena 内部不经过 isolation 参数创建 worktree,此改动不触碰 Arena 代码路径。

影响文件:

文件变更类型
packages/core/src/tools/agent/agent.ts新增 isolation 参数及 worktree 创建/清理逻辑
packages/core/src/tools/agent/fork-subagent.ts新增 buildWorktreeNotice() 并在 worktree 模式下注入
packages/core/src/services/gitWorktreeService.ts新增 createAgentWorktree() / removeAgentWorktree()
packages/core/src/services/worktreeCleanup.ts新建:过期 worktree 自动清理逻辑

Phase C:会话完整性(SessionService 持久化 + UI 安全网)

目标: worktree 状态在会话中断后可恢复,用户在界面上始终知道自己在哪个 worktree 里,退出会话时有安全提示。

要实现的功能:

SessionService worktree 状态持久化 + --resume 恢复:

  • SessionService 扩展 WorktreeSession 字段,记录 { slug, worktreePath, worktreeBranch, originalCwd, originalBranch }
  • EnterWorktreeTool 调用 sessionService.setWorktreeSession() 写入状态
  • ExitWorktreeTool 调用 sessionService.clearWorktreeSession() 清除状态
  • --resume 启动路径读取该字段,恢复 targetDir 并向模型注入上下文提示

Post-creation setup:

  • 创建 worktree 后自动执行 git config core.hooksPath <mainRepo>/.git/hooks,确保 worktree 内的提交与主仓库 hooks 行为一致

StatusLine worktree 展示:

  • UIStateContext 新增 activeWorktree 字段(从 session 状态读取),在会话进入 / 退出 worktree 时更新
  • StatusLineCommandInput payload 新增 worktree?: { slug: string; branch: string } 字段,供用户 statusline 脚本使用
  • FooteractiveWorktree 非空时内置展示一行 ⎇ <branch> (<slug>),无需用户配置 statusline 脚本即可获得基本可见性

WorktreeExitDialog:

  • 新增 WorktreeExitDialog.tsx 组件,参考现有 Dialog 写法
  • 修改退出键(Ctrl+C / Ctrl+D)处理逻辑:检测到 activeWorktree 非空时,拦截第二次确认,展示 Dialog 提示用户选择 keep 或 remove
  • keep / remove 操作复用 ExitWorktreeTool 的现有路径

影响文件:

文件变更类型
packages/core/src/services/sessionService.ts新增 WorktreeSession 字段及读写方法
packages/core/src/tools/enter-worktree.ts调用 sessionService.setWorktreeSession()
packages/core/src/tools/exit-worktree.ts调用 sessionService.clearWorktreeSession()
packages/core/src/services/gitWorktreeService.tscreateUserWorktree() / createAgentWorktree() 后追加 core.hooksPath 配置
packages/cli/src/ui/contexts/UIStateContext.tsx新增 activeWorktree 字段及 set/clear action
packages/cli/src/ui/hooks/useStatusLine.tsStatusLineCommandInput 新增 worktree 字段
packages/cli/src/ui/components/Footer.tsx内置 worktree 行展示
packages/cli/src/ui/components/WorktreeExitDialog.tsx新建
packages/cli/src/ui/components/DialogManager.tsx注册 WorktreeExitDialog
packages/cli/src/ui/components/ExitWarning.tsx 或退出键处理检测 activeWorktree 并拦截退出

Phase D:启动时配置(--worktree CLI 标志 + 目录符号链接 + PR 引用)

目标: 支持在启动时直接进入 worktree、通过目录符号链接减少大型项目的磁盘开销,以及通过 PR 引用快速基于一个 pull request 创建 worktree。

范围: 三个功能在一个阶段一起落地,因为它们都挂在同一个启动入口上,且 symlink / PR fetch 两者都需要在 worktree 创建之后立即执行 — 单独拆分会重复改 bootstrap 序列。

D-1:--worktree [name] CLI 启动标志

参数形态: yargs 选项接受三种形式:

形式行为
qwen --worktreebare flag,自动生成 slug({形容词}-{名词}-{6hex}
qwen --worktree my-name显式 slug,沿用 EnterWorktreeTool 的 slug 校验规则
qwen --worktree=my-name等价于上一种

不提供短别名 -w(qwen-code 短别名只保留给最高频参数,避免命名冲突)。

启动序列: worktree 在以下位置创建:

  1. parseArguments() 解析 argv(已有)
  2. resume picker(已有,line 588-629 of gemini.tsx
  3. loadCliConfig() 初始化 Config + auth(已有,line 643-653)
  4. 新增:argv.worktree !== undefined,调用 createUserWorktree()
    • 写入 sidecar(writeWorktreeSession()
    • 设置 process.chdir(worktreePath) 同时 Config.setTargetDir(worktreePath)
    • 同一 worktree 的 re-attach 路径:跳过 git worktree add 并就地 chdir(Phase 6 修复)。跨 projectHash 的 --resume × --worktree 组合在 session lookup 阶段会失败,详见下文"与 --resume 的优先级"。
  5. 主循环(TUI / headless -p / ACP 三种入口都要走第 4 步)

与 Phase A 简化的差异: Phase A 的 EnterWorktreeTool 修改 Config.targetDir,依赖模型从工具结果里读到绝对路径并继续使用。Phase D 的 CLI flag 在启动期就生效,没有运行中的模型上下文需要兼容,所以直接切换 targetDirprocess.cwd() —— 这是更强的隔离保证。两条路径行为不同,需要在用户文档里说明。

退出行为: 复用现有 WorktreeExitDialog(Phase C 已实现)。Ctrl+C/D 两次触发 → 用户在 keep / remove / cancel 之间选择。不需要新代码路径。

--resume 的优先级:

由于 session 存储以 projectHash(process.cwd()) 为 key,而 --worktree 在 resume picker / loadCliConfig 之前就 chdir 到 worktree,所以"在 worktree X 启动的 session,从 worktree Y 内 resume"是架构上不可达的(两者的 projectHash 不同,session 文件落在不同目录)。下表反映 D-1 实现 + Phase 6 re-attach 修复后的实际行为:

--resume 状态--worktree 状态结果
普通会话,无 worktree
有(新 slug)新建 worktree
有(已存在的 slug)re-attach 到已有 worktree(Phase 6 修复)
恢复旧 worktree(Phase C 行为,sidecar 命中则注入 reminder)
有(sid 出自同一 worktree)有(同一 slug,re-attach)re-attach + session 命中:正常 resume
有(sid 出自 main checkout)有(任意 slug)session lookup 失败No saved session found with ID …,exit 1。documented limitation
有(sid 出自 worktree X)有(slug Y, X != Y)同上,session 跨 projectHash 不可寻

跨 projectHash override 的语义(--worktree 在不同 worktree / 主 checkout 的 session 之间转移)需要 storage 锚定到 repo root 而非 cwd-derived projectHash,属于未来 Config 重构范畴。persistStartupWorktreeSidecar 内的 overrodeResumedWorktree 分支代码保留是为该重构落地后能自动生效,目前在生产路径不会触发。

D-2:worktree.symlinkDirectories 配置项

schema:

jsonc
{
  "worktree": {
    "symlinkDirectories": ["node_modules", "dist", ".turbo"],
  },
}
  • 类型:string[],默认 undefined(不开启,opt-in)
  • 顶层 namespace worktree 是新增的(在 settingsSchema.ts 中按字母序插在 toolsui 之间)
  • 路径相对于主仓库根,绝对路径或包含 .. 的路径被路径遍历守卫拒绝

作用范围: 所有由通用层创建的 worktree,包括:

  • EnterWorktreeTool(Phase A)
  • AgentTool isolation: 'worktree'(Phase B)
  • --worktree CLI flag(Phase D-1)

Arena 的 worktree 不走通用层,受此配置影响。

实现位置: GitWorktreeService.performPostCreationSetup() —— 紧跟现有的 configureHooksPath()(Phase C 已建立的模式)。新增 symlinkConfiguredDirectories() 方法,遍历配置项调用 fs.symlink(absSource, absDest, 'dir')

错误处理(fail-open):

场景行为
源目录不存在(ENOENT)静默跳过,debug log
目标路径已存在(EEXIST)静默跳过,debug log(不覆盖)
路径遍历(../、绝对路径等)拒绝该项,debug log warn
其他 I/O 错误debug log warn,继续处理后续项

worktree 创建本身不会因为 symlink 失败而中止 —— 与 configureHooksPath() 相同的"best-effort post-creation setup"原则。

D-3:PR 引用解析(--worktree=#<N> / 全 URL)

支持形式:

形式解析后的 PR 号
--worktree=#123123
--worktree '#123'123
--worktree https://github.com/foo/bar/pull/123123
--worktree https://gh.enterprise.com/foo/bar/pull/123?baz=qux123

slug 与分支命名:

  • slug:pr-<N>(特殊保留前缀,与用户 slug 区分)
  • 分支:worktree-pr-<N>(沿用 qwen-code 现有 worktree-<slug> 命名规则;不采用 claude-code 的 pr-<N> 直接命名,避免与本地 pr-<N> 分支冲突)

fetch 策略:

git fetch origin pull/<N>/head
→ 用 FETCH_HEAD 作为新 worktree 的 base

不依赖 gh CLI —— 纯 git fetch,支持任何 GitHub 实例(公网或企业版),只要 origin 远程指向 GitHub。

错误路径:

场景错误消息
origin 远程缺失--worktree=#<N> requires an "origin" remote that points at GitHub.
git fetch 失败Failed to fetch PR #<N>: PR may not exist or origin remote is unreachable.
网络超时(30s)同上,加 (timeout)
origin 远程不是 GitHub不做主动检查,由 git fetch 自然失败(PR 协议是 GitHub 特有的)

与 D-2 的关系: PR worktree 同样应用 symlinkDirectories(用户期望在 PR 上立刻能跑测试,依赖目录需要复用)。

影响文件

文件变更类型
packages/cli/src/config/config.tsyargs 新增 --worktree 选项;CliArgs 接口加 worktree?: string | boolean
packages/cli/src/gemini.tsxloadCliConfig() 之后、主循环之前调用新的 setupStartupWorktree() helper
packages/cli/src/startup/worktreeStartup.ts新建:setupStartupWorktree() 处理 slug 解析、PR fetch、sidecar 写入、cwd 切换
packages/cli/src/nonInteractiveCli.ts复用同一 helper(已有 restoreWorktreeContext 注入逻辑,无须改)
packages/cli/src/acp-integration/acpAgent.ts复用同一 helper
packages/core/src/services/gitWorktreeService.ts新增 parsePRReference()fetchPullRequestRef()symlinkConfiguredDirectories()createUserWorktree() 接受可选 baseBranchRef 参数
packages/cli/src/config/settingsSchema.ts新增 worktree.symlinkDirectories: string[] 顶层项
packages/vscode-ide-companion/schemas/settings.schema.json重新生成
docs/users/features/worktree.md新增 Quick Start CLI flag 章节、Settings 表新增一行

安全与回滚

  • fail-open vs fail-close: symlink / hooks 失败 中止 worktree 创建(同 Phase C 既定模式);PR fetch 失败 中止 启动(无 base ref 就无法创建 worktree);slug 校验失败 中止 启动(与 EnterWorktreeTool 一致)。
  • path traversal: symlinkDirectories 项必须解析后仍在 repoRoot 内,否则拒绝该项并 log。
  • PR fetch 超时: 30 秒硬超时,避免无响应的网络拖死启动。
  • cwd 切换的副作用:process.cwd() 之后,相对路径(如 --prompt-file ./foo.txt)的解析会受影响。对策: 在切 cwd 之前先解析所有相对路径参数(具体在 setupStartupWorktree() 入口处做一次 normalize)。

开放问题

  1. --worktree-keep-on-exit claude-code 没有,qwen-code 是否需要一个 CLI flag 让 Exit Dialog 默认选 keep?建议先不加,等用户反馈。
  2. worktree.symlinkDirectories 是否需要 per-project override? 当前 settings 已经支持 user/workspace/project 三级合并,无需特殊处理。
  3. PR fetch 是否要拉取 merge ref(pull/<N>/merge,即与 base 合并后的 ref)而非 head claude-code 选 head,理由是用户通常想看 PR 的实际改动。沿用此选择。

Future:高级功能(按需实现)

以下功能面向更特定的使用场景,当前阶段不纳入排期,待用户需求明确后再评估实现。

功能说明
sparse checkoutworktree.sparsePaths 配置项,大型 monorepo 只 checkout 指定路径,缩短创建时间和磁盘占用
.worktreeinclude 文件将 gitignore 的文件(.envsecrets.json 等)自动复制进 worktree
tmux 集成--worktree --tmux 在新 tmux 窗口启动 worktree 会话