docs/design/worktree.md
qwen-code 目前仅有面向 Arena 多模型对比场景的内部 worktree 实现(GitWorktreeService),用户无法在普通会话中使用 worktree 隔离工作,AgentTool 也不支持为 subagent 创建隔离的 worktree 环境。
目标是将 worktree 做成通用能力,支持用户会话级隔离和 Agent 级隔离,同时保证现有 Arena 功能体验完全不变。
| 功能 | qwen-code | claude-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 checkout | ❌ | ✅ | Future |
| tmux 集成 | ❌ | ✅ | Future |
| Arena 多模型 worktree 隔离 | ✅(qwen 独有) | ❌ | — |
| 脏状态覆盖(stash + copy) | ✅ | ✅ | — |
| Baseline commit 追踪 | ✅(qwen 独有) | ❌ | — |
worktree 是通用能力,Arena 是其上层应用。
EnterWorktree/ExitWorktree 工具、AgentTool isolation 参数、会话状态管理、自动清理worktreeBaseDir 自定义路径、批量创建与 diff 对比,继续使用 GitWorktreeService.setupWorktrees() 的现有逻辑,不受通用层改动影响AgentTool 的 isolation: 'worktree' 只走通用路径,Arena 内部不经过这个参数创建 worktree,两者路径独立。
由 EnterWorktree 工具或 AgentTool isolation: 'worktree' 创建的 worktree 固定存放在:
{git 仓库根}/.qwen/worktrees/{slug}
路径不可配置。slug 命名规则:
{形容词}-{名词}-{4位随机})agent-{7位随机 hex}Arena 的 worktree 路径由 agents.arena.worktreeBaseDir 控制,默认 ~/.qwen/arena(ArenaManager.ts:125),与通用路径完全独立,不做任何改动。
| 配置项 | 类型 | 用途 | 阶段 |
|---|---|---|---|
ui.hideBuiltinWorktreeIndicator | boolean | 隐藏 Footer 中内置 ⎇ worktree-… (…) 行,留给 custom statusline | Phase C |
worktree.symlinkDirectories | string[] | 符号链接指定目录(如 node_modules)到 worktree,避免磁盘浪费 | Phase D |
worktree.sparsePaths | string[] | git sparse-checkout cone 模式,大型 monorepo 只写入指定路径 | Future |
Phase A / B 不新增任何配置项。
触发条件: 用户明确说 "start a worktree"、"use a worktree"、"create a worktree" 等词语。不应在用户说"修复 bug"、"开发功能"时自动触发。
输入 schema:
name?: string // 可选,slug 格式:字母/数字/点/下划线/破折号,最大 64 字符
行为:
GitWorktreeService 创建 worktree,路径为 .qwen/worktrees/{slug}SessionService输出: worktreePath、worktreeBranch、message
触发条件: 用户说 "exit the worktree"、"leave the worktree"、"go back" 等。
输入 schema:
action: 'keep' | 'remove'
discard_changes?: boolean // 仅 action='remove' 时有效
安全守卫:
EnterWorktree 创建的 worktreeaction='remove' 且存在未提交变更时,拒绝执行(除非 discard_changes: true)行为:
keep:清空会话中的 worktree 状态,保留 worktree 目录和分支,恢复原始工作目录remove:删除 worktree 目录,删除对应 git 分支,清空会话状态,恢复原始工作目录输出: action、originalCwd、worktreePath、worktreeBranch
| 方式 | 示例 | 实现阶段 |
|---|---|---|
| 会话中明确请求 | 用户说 "在 worktree 中开始工作" → 模型调用 EnterWorktree | Phase A |
| Agent 隔离 | 模型为 subagent 设置 isolation: 'worktree' | Phase B |
| CLI 启动标志 | qwen --worktree my-feature | Phase D |
无斜杠命令。会话中 worktree 的触发依赖用户明确提及,isolation: 'worktree' 才是模型自主决策的场景。
目标: 用户能在会话中进入 / 退出 worktree。
要实现的功能:
EnterWorktree 工具:创建 worktree,切换工作目录,记录会话状态ExitWorktree 工具:keep / remove 两种退出方式,安全守卫GitWorktreeService 扩展:新增面向单用户会话的 createUserWorktree() / removeUserWorktree() 方法,复用现有 git 操作逻辑,不改动 Arena 使用的批量接口SessionService 扩展:新增 WorktreeSession 字段,记录 { slug, worktreePath, worktreeBranch, originalCwd, originalBranch };--resume 时恢复 worktree 工作目录影响文件:
| 文件 | 变更类型 |
|---|---|
packages/core/src/tools/tool-names.ts | 新增 ENTER_WORKTREE、EXIT_WORKTREE 常量 |
packages/core/src/tools/EnterWorktreeTool/ | 新建目录:EnterWorktreeTool.ts、prompt.ts |
packages/core/src/tools/ExitWorktreeTool/ | 新建目录:ExitWorktreeTool.ts、prompt.ts |
packages/core/src/services/gitWorktreeService.ts | 新增用户会话级接口(不改动 Arena 接口) |
packages/core/src/services/sessionService.ts | 新增 WorktreeSession 字段及读写方法 |
packages/core/src/tools/ 注册入口 | 注册新工具 |
不在 Phase A 范围内:
isolation: 'worktree')+ 描述更新目标: 模型可为 subagent 创建临时隔离 worktree,agent 结束后自动清理;同步更新受影响的工具描述和提示词。
要实现的功能:
Agent 隔离核心:
AgentTool 新增 isolation?: 'worktree' 参数agent-{7hex},路径:.qwen/worktrees/agent-{7hex}).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)无需改动:
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 自动清理逻辑 |
目标: worktree 状态在会话中断后可恢复,用户在界面上始终知道自己在哪个 worktree 里,退出会话时有安全提示。
要实现的功能:
SessionService worktree 状态持久化 + --resume 恢复:
SessionService 扩展 WorktreeSession 字段,记录 { slug, worktreePath, worktreeBranch, originalCwd, originalBranch }EnterWorktreeTool 调用 sessionService.setWorktreeSession() 写入状态ExitWorktreeTool 调用 sessionService.clearWorktreeSession() 清除状态--resume 启动路径读取该字段,恢复 targetDir 并向模型注入上下文提示Post-creation setup:
git config core.hooksPath <mainRepo>/.git/hooks,确保 worktree 内的提交与主仓库 hooks 行为一致StatusLine worktree 展示:
UIStateContext 新增 activeWorktree 字段(从 session 状态读取),在会话进入 / 退出 worktree 时更新StatusLineCommandInput payload 新增 worktree?: { slug: string; branch: string } 字段,供用户 statusline 脚本使用Footer 在 activeWorktree 非空时内置展示一行 ⎇ <branch> (<slug>),无需用户配置 statusline 脚本即可获得基本可见性WorktreeExitDialog:
WorktreeExitDialog.tsx 组件,参考现有 Dialog 写法activeWorktree 非空时,拦截第二次确认,展示 Dialog 提示用户选择 keep 或 removeExitWorktreeTool 的现有路径影响文件:
| 文件 | 变更类型 |
|---|---|
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.ts | createUserWorktree() / createAgentWorktree() 后追加 core.hooksPath 配置 |
packages/cli/src/ui/contexts/UIStateContext.tsx | 新增 activeWorktree 字段及 set/clear action |
packages/cli/src/ui/hooks/useStatusLine.ts | StatusLineCommandInput 新增 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 并拦截退出 |
--worktree CLI 标志 + 目录符号链接 + PR 引用)目标: 支持在启动时直接进入 worktree、通过目录符号链接减少大型项目的磁盘开销,以及通过 PR 引用快速基于一个 pull request 创建 worktree。
范围: 三个功能在一个阶段一起落地,因为它们都挂在同一个启动入口上,且 symlink / PR fetch 两者都需要在 worktree 创建之后立即执行 — 单独拆分会重复改 bootstrap 序列。
--worktree [name] CLI 启动标志参数形态: yargs 选项接受三种形式:
| 形式 | 行为 |
|---|---|
qwen --worktree | bare flag,自动生成 slug({形容词}-{名词}-{6hex}) |
qwen --worktree my-name | 显式 slug,沿用 EnterWorktreeTool 的 slug 校验规则 |
qwen --worktree=my-name | 等价于上一种 |
不提供短别名 -w(qwen-code 短别名只保留给最高频参数,避免命名冲突)。
启动序列: worktree 在以下位置创建:
parseArguments() 解析 argv(已有)gemini.tsx)loadCliConfig() 初始化 Config + auth(已有,line 643-653)argv.worktree !== undefined,调用 createUserWorktree()
writeWorktreeSession())process.chdir(worktreePath) 同时 Config.setTargetDir(worktreePath)git worktree add 并就地 chdir(Phase 6 修复)。跨 projectHash 的 --resume × --worktree 组合在 session lookup 阶段会失败,详见下文"与 --resume 的优先级"。-p / ACP 三种入口都要走第 4 步)与 Phase A 简化的差异: Phase A 的 EnterWorktreeTool 不修改 Config.targetDir,依赖模型从工具结果里读到绝对路径并继续使用。Phase D 的 CLI flag 在启动期就生效,没有运行中的模型上下文需要兼容,所以直接切换 targetDir 和 process.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 分支代码保留是为该重构落地后能自动生效,目前在生产路径不会触发。
worktree.symlinkDirectories 配置项schema:
{
"worktree": {
"symlinkDirectories": ["node_modules", "dist", ".turbo"],
},
}
string[],默认 undefined(不开启,opt-in)worktree 是新增的(在 settingsSchema.ts 中按字母序插在 tools 与 ui 之间).. 的路径被路径遍历守卫拒绝作用范围: 所有由通用层创建的 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"原则。
--worktree=#<N> / 全 URL)支持形式:
| 形式 | 解析后的 PR 号 |
|---|---|
--worktree=#123 | 123 |
--worktree '#123' | 123 |
--worktree https://github.com/foo/bar/pull/123 | 123 |
--worktree https://gh.enterprise.com/foo/bar/pull/123?baz=qux | 123 |
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.ts | yargs 新增 --worktree 选项;CliArgs 接口加 worktree?: string | boolean |
packages/cli/src/gemini.tsx | loadCliConfig() 之后、主循环之前调用新的 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 表新增一行 |
EnterWorktreeTool 一致)。symlinkDirectories 项必须解析后仍在 repoRoot 内,否则拒绝该项并 log。process.cwd() 之后,相对路径(如 --prompt-file ./foo.txt)的解析会受影响。对策: 在切 cwd 之前先解析所有相对路径参数(具体在 setupStartupWorktree() 入口处做一次 normalize)。--worktree-keep-on-exit? claude-code 没有,qwen-code 是否需要一个 CLI flag 让 Exit Dialog 默认选 keep?建议先不加,等用户反馈。worktree.symlinkDirectories 是否需要 per-project override? 当前 settings 已经支持 user/workspace/project 三级合并,无需特殊处理。merge ref(pull/<N>/merge,即与 base 合并后的 ref)而非 head? claude-code 选 head,理由是用户通常想看 PR 的实际改动。沿用此选择。以下功能面向更特定的使用场景,当前阶段不纳入排期,待用户需求明确后再评估实现。
| 功能 | 说明 |
|---|---|
| sparse checkout | worktree.sparsePaths 配置项,大型 monorepo 只 checkout 指定路径,缩短创建时间和磁盘占用 |
.worktreeinclude 文件 | 将 gitignore 的文件(.env、secrets.json 等)自动复制进 worktree |
| tmux 集成 | --worktree --tmux 在新 tmux 窗口启动 worktree 会话 |