跳到主要内容

数据截至 (上游 commit 4ec6bbf5884e)

一条 worktree 的一生 — 从选 base 到安全清场

30 秒导读: Orca 卖点是"一堆 agent 各干各的,互不打架",这个卖点的落地处就是 worktree —— 每个 agent 一个独立工作目录、独立分支。本章端到端追一次 workspace 的真实路径:选 base → 定名字 → git worktree add → 装修目录 → 起 setup 和 agent → 首条消息改名 → 删除清场。重点不是"怎么调 git",而是"为什么每一步都要先证明再动手"。

本章不讲终端进程本身怎么起(见 终端底座),也不重复 Project / Worktree / Tab 的数据结构(见 领域模型)。


1. 先说清楚:Orca 说的 "worktree" 是什么

一句话定义: git worktree(Git 工作树)是 Git 自带的功能 —— 同一个仓库、同一份对象库,可以在磁盘上摊开成多个独立的工作目录,各自 checkout 不同分支

为什么 Orca 需要它。 你要同时跑五个编码 agent。如果它们共用一个目录,A 改了 src/foo.ts 而 B 正在跑测试,两边互相污染。给每个 agent 一份完整 clone?磁盘和 fetch 成本都爆掉。worktree 是中间答案:对象库共用一份,工作目录各自一份。

手工做这件事只要一行:

git worktree add --no-track -b feature/login ../workspaces/login origin/main

Orca 在这一行前后做的事,才是本章的内容:

阶段Orca 额外做了什么为什么必须做
之前解析 base ref、探测它是否过期、按需 fetch用户没指定 base 时不能瞎猜;指定的 base 可能早已不存在
之前分支名/目录名冲突时自动加后缀重试agent 批量建 workspace,撞名是常态而非异常
之后软链 / 复制配置文件、node_modules 等新工作目录是空的,setup 脚本跑不起来
之后决定 setup 脚本和 agent 谁先启动agent 在依赖装完前开工 = 满屏报错
使用中按首条消息把 you/Nautilus 改成有意义的分支名创建时还不知道这个 workspace 要干什么
删除五道防线证明"这确实是我建的、可以删"递归删目录一旦判错,删的是用户的代码

一句话直觉: 把 worktree 当成租客。Git 是房东(管注册),Orca 是中介 —— 它负责签约(选 base、定名)、装修(软链/复制)、通水电(setup + agent),以及退租时先证明这间房确实是它租出去的,再拆装修。本章最长的一节是"退租",原因就在这句话的后半段。


2. 顶层全景:一生的六个阶段

先看整条路径。从左到右是时间顺序,方框里第一行是白话阶段名,第二行是主要落地函数。

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ① 选 base │──▶│ ② 定名字/路径 │──▶│ ③ 建工作树 │
│ 三级优先级 │ │ 后缀重试循环 │ │ worktree add │
│ + 过期探测 │ │ 冲突分类 │ │ + 三个副作用 │
└──────────────┘ └──────────────┘ └──────────────┘

┌─────────────────────────────────────┘

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ④ 装修目录 │──▶│ ⑤ 起 setup │──▶│ ⑥ 清场 │
│ 链/共享/复制 │ │ 与 agent │ │ 五道防线 │
│ + 复制预算 │ │ 立即 or 等待 │ │ + 平台恢复 │
└──────────────┘ └──────────────┘ └──────────────┘

▼ 首条 agent 消息到达时
┌──────────────────┐
│ 自动改名(旁路) │
│ 分支 + 侧栏标题 │
└──────────────────┘

入口只有两个 IPC 通道,两边都在 src/main/ipc/worktrees.ts 注册:

通道位置干什么
worktrees:prefetchCreateBasesrc/main/ipc/worktrees.ts:2233用户还在填表时先把 base 的网络请求跑掉
worktrees:createsrc/main/ipc/worktrees.ts:2248真正创建
worktrees:removesrc/main/ipc/worktrees.ts:2474真正删除
worktrees:forgetLocalsrc/main/ipc/worktrees.ts:3170只忘掉本地记录,不碰远端磁盘

worktrees:create 本身很薄,它只做一件事:按仓库类型分三条路src/main/ipc/worktrees.ts:2276-2280)。

仓库类型走哪个函数说明
folder 项目(非 git)createFolderWorkspace没有 worktree 可言,只造一条元数据
SSH 远端仓库createRemoteWorktreesrc/main/ipc/worktree-remote.ts:1512所有 git 操作走 relay RPC
本地 / WSL 仓库createLocalWorktreesrc/main/ipc/worktree-remote.ts:1960本章主线

后面的 §3–§5 全部沿 createLocalWorktree 往下走。它是一个约 680 行的长函数,但结构非常线性 —— 就是上面那张图。


3. 第一步:base ref 从哪来

3.1 它要解决的小问题

用户点"新建 workspace",可能没选 base branch。Orca 得替他决定:从 origin/main 开?从这个仓库上次记住的 base 开?还是直接报错?

难点不是"挑一个",而是"挑的那个可能已经不存在了"。 仓库配置里存的 worktreeBaseRef 是持久化数据,用户可能半年前设过 origin/develop,而这个分支早被删了。拿一个不存在的 ref 去喂 git worktree add,得到的是一句看不懂的 git 报错。

3.2 三级优先级

决策逻辑被单独抽成一个 29 行的纯函数 resolveWorktreeCreateBasesrc/main/worktree-create-base.ts:8),不碰 git、不碰文件系统,全部依赖注入进来。从上往下命中即停:

优先级条件结果
1用户在 UI 里显式选了 base直接用,不做任何探测
2仓库存了 worktreeBaseRef,且它 == 探测出的默认 base用它
3仓库存了 worktreeBaseRef,但和默认 base 不同探测它还能不能用:能用就用(自定义 base 保留权威性),不能用就退回默认 base
4什么都没有用探测出的默认 base;连默认都没有则返回 null

第 3 条就是 stale-base(过期 base)探测,源码里的注释直说:

// Stale persisted refs fall back to the detected default; usable custom refs stay authoritative.
return (await args.isBaseUsable(repoWorktreeBaseRef)) ? repoWorktreeBaseRef : defaultBaseRef

src/main/worktree-create-base.ts:27-28

"探测出的默认 base"本身也有固定顺序 —— 先问 origin/HEAD 的符号目标,问不到就按一张固定表逐个试(src/main/git/repo.ts:40DEFAULT_BASE_REF_PROBESorigin/mainorigin/mastermainmaster)。这张表被本地和 SSH 两条路径共用,注释明说是为了"防止两个 transport 的探测顺序漂移"。

全部落空时不静默兜底。 createLocalWorktree 会抛一句人话(src/main/ipc/worktree-remote.ts:2025-2029):

Could not resolve a default base ref for this repo. Pick a base branch explicitly and try again.

注释解释了为什么不硬编码一个 main 顶上去:那样只会把错误推迟到 git worktree add 里,变成一句不可读的 git 报错。

3.3 base 存不存在,要分三种问法

确定 base 名字之后,还要回答"这个 ref 在本地有没有、要不要联网拉"。Orca 把它拆成三种情况处理(src/main/ipc/worktree-remote.ts:2041-2111):

情况怎么判断动作
base 是远程跟踪分支(origin/main 这种)resolveRemoteTrackingBase 能把它拆成 remote + branch起一次精确 refresh:只 fetch 这一个 ref
base 是普通本地名(main / 某个本地分支)拆不出 remote,且本地没有该 commit起一次尽力而为的全量 fetch origin,失败也不阻塞
base 已经是本地可用的 commit / 已验证的 PR SHA本地 ref 探测通过什么都不做

resolveRemoteTrackingBasesrc/main/runtime/orca-runtime.ts:25871)值得单看:它先 git remote 列出真实 remote 名,再按名字长度倒序匹配前缀。这不是过度设计 —— git 允许 remote 名里带斜杠,foo/bar/feature 到底是 remote foobar/feature 还是 remote foo/barfeature,只能靠真实 remote 列表裁决。

精确 refresh 的 fetch 是这条命令src/main/runtime/orca-runtime.ts:25827-25834):

git -c maintenance.auto=false fetch --no-tags <remote> +refs/heads/<branch>:refs/remotes/<remote>/<branch>

三个细节都有理由:--no-tags 不拖标签;+ 强制更新;前置的 -c 抑制 auto-maintenance,免得创建路径被后台 gc 拖住。

3.4 离线时的降级:baseFallback

如果精确 refresh 做不了(比如断网),但本地恰好有一个能用的分支,Orca 不报错,而是降级并把降级事实记下来(src/main/ipc/worktree-remote.ts:2065-2076):

// Why: use the usable local branch when offline refresh cannot create its tracking ref.
if (hasFallbackLocalBaseRef) { baseBranch = remoteTrackingBase.branch }
baseFallback = { requestedRef: remoteTrackingBase.base, localRef: baseBranch }

baseFallback 最终会随创建结果返回给渲染进程(src/main/ipc/worktree-remote.ts:2660),UI 得以告诉用户"你要的是 origin/main,实际用的是本地 main"。降级本身不可怕,静默降级才可怕。

反过来,如果 refresh 是必需的(本地根本没有这个 base),那 refresh 失败就必须阻断创建(src/main/ipc/worktree-remote.ts:2301-2320):

if (!result.ok && !remoteTrackingRefresh.hadLocalBaseRef) {
throw new Error(`Could not refresh base ref "${baseBranch}" from "..."`)
}

判据是 hadLocalBaseRef —— 有旧 ref(哪怕陈旧)就放行,一点都没有才拦。

3.5 prefetch:把网络提前到用户还在打字时

创建路径里最慢的一步永远是 fetch。Orca 给它加了一个纯优化通道:渲染进程一旦知道用户选了哪个仓库/哪个 base,就先发 worktrees:prefetchCreateBase,主进程跑一遍 prefetchWorktreeCreateBasesrc/main/worktree-create-base-prefetch.ts:95)。

这个通道的设计原则是绝对不能坏事,三处体现:

  • IPC 层整个 try/catch 吞掉异常(src/main/ipc/worktrees.ts:2240-2244),注释写明"真正的 create 会 await 同一个 refresh 并在那里报错"。
  • 它复用同一套 base 决策函数和同一个 fetch 缓存,所以真 create 命中的是同一条 in-flight promise,不会重复打网络。
  • 它自己也会跳过明显无意义的情况:folder 仓库直接 return,已经是本地对象的 full SHA 直接 return(src/main/worktree-create-base-prefetch.ts:69-74)。

fetch 缓存本身在 getOrStartRemoteFetch / getOrStartRemoteTrackingBaseRefreshsrc/main/runtime/orca-runtime.ts:25748 / :22289)。两条不变量写得很直白:

  • 只在成功时写时间戳,否则"新鲜度缓存"会撒谎(:22266-22268)。
  • 成功和失败都要从 in-flight map 里剔除,否则一次失败的 fetch 会把这个仓库后续所有创建永久卡死(:22278-22283)。

还有一条更微妙的:全量 fetch 的新鲜度不能证明精确 base 的新鲜度,因为仓库的 refspec 可能把某些分支排除在外,所以两者用不同的缓存 key(:22300-22304)。


4. 第二步:定名字和路径(后缀重试循环)

4.1 为什么要循环

agent 批量建 workspace 时,"名字已被占用"是常态。可能撞的东西有四种:本地分支、远端分支、已存在的 PR、磁盘上已有的目录。挨个报错让用户改名,体验就崩了。

Orca 的做法是一个最多 100 次的后缀循环(src/main/worktree-create-candidates.ts:8WORKTREE_CREATE_MAX_SUFFIX_ATTEMPTS),候选名生成规则简单到只有一行:第 1 次用原名,之后是 name-2name-3src/main/worktree-create-candidates.ts:10)。

循环体在 src/main/ipc/worktree-remote.ts:1960 起的 createLocalWorktree 内。一次迭代按这个顺序过五关,任何一关不过就 continue 换下一个后缀:

候选名 name-N


① 算出分支名 ────────────── 前缀策略 + check-ref-format
│ 通过

② 能否直接 checkout 已有本地分支?── 是 → 记住它,跳过③
│ 否

③ 分支冲突分类 ───────────── local / remote / 无
│ 无冲突(或属于允许的 push-target 冲突)

④ 已存在 PR? ───────────── 仅当 suffix>1 才查(省一次网络)
│ 无

⑤ 目录已存在? ──────────── existsSync
│ 否

采纳

4.2 三处值得学的细节

① PR 查询被推迟到第二轮。 gh pr list 是 1–3 秒的网络调用。源码注释直接把这个权衡写出来了(src/main/ipc/worktree-remote.ts:2248):

// Why: gh pr list is a ~1–3s network call; only probe PR conflicts after a branch
// collision (suffix > 1) so the common no-collision path skips it.

没撞名 = 绝大多数情况 = 零网络开销。

② "checkout 已有分支"要锁死选择。 一旦第 N 轮认定这是一次"检出用户已有分支",就把分支名记进 selectedExistingLocalBranchName,后续轮次只换路径后缀、不换分支(src/main/ipc/worktree-remote.ts:2199-2202)。否则用户想打开 feature/login,重试一次就给他打开了 feature/login-2,属于静默做错事。

③ 分支名前缀失败要分情况。 前缀策略可以是 git 用户名。而 gh api 被限流时会吐一坨 JSON,被当成"用户名"就会生成非法分支名。computeValidatedBranchNamesrc/main/ipc/worktree-branch-name.ts:44)对此的处理是按来源区别对待

前缀来源前缀非法时理由
git-username(自动探测)静默丢掉前缀,继续创建探测失败不该阻塞每一次创建
用户自定义直接抛错用户手写的配置错了必须响亮报出来

4.3 路径怎么算,以及一道围栏

路径由 computeWorktreePathsrc/main/ipc/worktree-logic.ts:101)算:workspace 根目录 +(可选的仓库名一层)+ 名字。WSL 仓库有专门分支 —— 工作树必须留在 WSL 文件系统内,否则跨文件系统 I/O 慢到不可用,终端也会开成 Windows shell(src/main/ipc/worktree-logic.ts:94-100 的注释)。

算完立刻过一道围栏 ensurePathWithinWorkspacesrc/main/ipc/worktree-logic.ts:79):解析成绝对路径后,如果相对 workspace 根是 .. 开头或绝对路径,抛 Invalid worktree path

名字的清洗在 sanitizeWorktreeNamesrc/main/ipc/worktree-logic.ts:25)。它刻意保留 Unicode 字母数字(中文名字的 workspace 是合法的),只剔除 git 和文件系统真正拒绝的字符;其中一条专门治路径穿越:

// git check-ref-format rejects any ref containing `..`, so a prompt like
// "../../foo" that survives slugification as `..-..-foo` would produce a
// branch name git refuses to create.
.replace(/\.{2,}/g, '.')

还有一个容易忽略的准备动作:主进程启动时会对每个本地仓库 mkdir 一次 workspace 根目录(src/main/worktree-root-preparation.ts:14),注释说明这是为了预热 macOS 的 TCC 权限弹窗 —— 让权限对话框出现在空闲时刻,而不是用户点"创建"的那一秒。


5. 第三步:建工作树,然后"装修"

5.1 git worktree add 的三个副作用

真正落地在 addWorktreesrc/main/git/worktree.ts:941)。除了那行 add,它还有三个 best-effort 的副作用,每个都写清了理由:

副作用位置为什么
--no-tracksrc/main/git/worktree.ts:987不继承 base 的 upstream,否则新分支一建出来 git status 就报"落后 N 个提交"
branch.<name>.base:985 persistWorktreeCreationBase记住创建时的 base,供后续 diff/对比使用
push.autoSetupRemote=true:993-1012补上 --no-track 的代价:让第一次裸 git push 能自动建 upstream

第三个的读判断很讲究:用 git config --get(不加 --local),任何 scope 有值都算"用户已经选过",不覆盖;而且只有退出码恰好为 1(各 scope 均未设置)才算"未设置",其他非零码视为真实读失败并向上抛(:1001-1007)。

SSH 侧有一份镜像实现 addWorktreeOpsrc/relay/git-handler-worktree-ops.ts:31),注释明确要求两边锁步修改,理由是"不能让同一个操作因为仓库是本地还是 SSH 而产生不同的 git status / git push 体验"。relay 侧还多一道输入防御(src/relay/git-handler-worktree-ops.ts:41):分支名或 base 以 - 开头直接拒绝,否则会被 git 当成 flag 解析,--detach 之类可以改变整条命令的语义。

base ref 在传给 git 之前还要过一次消歧(src/shared/worktree/base-ref.ts:3 resolveWorktreeAddBaseRef):git worktree add 收的是 revision,短名字可能和 tag 撞。带斜杠的优先按 refs/remotes/ 解释,不带斜杠的按 refs/heads/ —— 这正好对应 Orca base picker 展示给用户的命名空间。

5.2 建完先"认领"回来

add 成功不代表路径就是你写的那个 —— git 可能把符号链接路径规范化掉。所以要重新 list 一次,靠路径 + 分支联合定位刚建出来那行(src/main/ipc/worktree-remote.ts:2465-2474findCreatedWorktree),找不到就抛 Worktree created but not found in listing

然后写元数据。这里有一条不显眼但关键的(src/main/ipc/worktree-remote.ts:2481-2482):

// Why: path-derived IDs can be reused after external deletion; rotate instance identity
// so stale lineage can't attach to the new occupant.
instanceId: randomUUID(),

worktree id 是路径派生的(repoId::path)。同一个路径被删了又建,id 一模一样。instanceId 是每次创建重新生成的 UUID,用来把"上一任租客"的血缘/会话数据挡在门外。

5.3 装修:三种物化模式

新工作目录里没有 node_modules、没有 .env,setup 脚本大概率跑不起来。Orca 提供三种把主检出目录的东西"搬"过来的方式,统一走 materializeWorktreePathssrc/main/ipc/worktree-symlinks.ts:191),靠一个 mode 参数分岔(src/main/ipc/worktree-symlinks.ts:36):

mode配置来源APFS 可用时APFS 不可用时语义
link用户设置的 symlinkPathsAPFS clone软链尽量各自一份,退化为共享
shareorca.yamlsharedDirectories永远软链软链一次安装服务所有 worktree
copy仓库里的 .worktreeincludeAPFS clone真复制,绝不软链每个 worktree 私有,改动不能漏回主目录

share 永不 clone 的理由写在类型定义旁:clone 会让每个 worktree 拿到独立的 node_modules,正好废掉"一次安装服务所有人"这个目的。

调用顺序也有讲究 —— 软链在 setup 之前做(src/main/ipc/worktree-remote.ts:2549-2555):

// Why: link user-configured shared paths (e.g. `node_modules`, `.env`) before setup
// runs so setup scripts see them in place.

5.4 APFS clonefile:一个平台特化的加速

macOS 的 APFS 支持 copy-on-write 克隆,2.7 GB 的目录克隆约 20ms、且不占额外磁盘。cloneWorktreePathWithApfssrc/main/ipc/worktree-apfs-clone.ts:201)用的是 /bin/cp -c,而不是 Node 的 COPYFILE_FICLONE_FORCE(注释说后者在他们的运行时里返回 ENOSYS)。

关键是它先探测卷再动手:209 assertSameApfsVolume),因为 cp -c 在非 APFS 上会静默退化成全量复制 —— 那就从"20ms"变成"几分钟",用户完全无从察觉。

探测本身要跑 df + diskutil 两个子进程。复制 N 个路径就是 4N 个子进程,所以按 stat().dev 做了缓存(src/main/ipc/worktree-apfs-clone.ts:36DarwinFilesystemCache),把它压到"每个不同卷一次"。

发布环节的两处并发防御也很典型:

  • 文件用临时路径 + link(2) 原子发布(:157),因为 rename(2) 会覆盖掉在检查之后才出现的目标。
  • 目录mkdir 占位再往里 cp -n -R:178:191),失败时只 rmdir 空占位 —— 如果里面已经有东西,宁可留给用户/git 检查也不递归删。

5.5 复制预算:拦住 node_modules

.worktreeinclude仓库作者写的列表,Orca 的用户不一定控制得了。一个把 node_modules 写进去的仓库,会让每次创建卡几分钟。

于是有了 createWorktreeCopyBudgetTrackersrc/main/ipc/worktree-include-copy-budget.ts:130),默认上限 2 GB / 50000 个条目(:18)。三个设计点:

① 先量后拷,绝不半途中止。 理由写在函数注释里(:125-129):fs.cp 会忽略 signal 选项,一旦开始就停不下来,中止只会留下半棵树。在写第一个字节之前拒绝,才能让 worktree 停在用户能理解的状态。

② clone 模式下不计字节,只计 inode。 因为 APFS 克隆的字节是免费的,按字节拒绝会拒掉本来不花钱的活(src/main/ipc/worktree-symlinks.ts:248-257)。如果之后 clone 真的失败、要退回真复制,才把当初没记的字节补记上,补不进就拒绝这一项(src/main/ipc/worktree-symlinks.ts:143-145)—— 不能让 fallback 偷偷绕开预算。

③ "丈量"本身也要限额。 被拒的条目不消耗复制预算,那么一个列了 1000 个超标目录的 .worktreeinclude 会付 1000 次满额遍历,正好复现要治的卡顿。所以另设一个 walk 上限(maxEntries × 5:26:138),并且超支原因要如实归因 —— 因为前面条目耗尽了丈量额度而被拒的,reason 记成 sizing 而不是 entries,因为"引用一个它从没接近过的限额,是在骗用户"(:96-98:202-203)。

被拒的条目会汇总成一句人话警告返回给 UI(formatWorktreeIncludeCopyWarning:181),最多点名 5 个,其余折叠成"and N more"。


6. 第四步:setup 脚本和 agent,谁先跑

这是本章最容易被低估的一节。它是两个正交的决策,不是一个。

6.1 决策一:setup 到底跑不跑

shouldRunSetupForCreatesrc/main/effective-hook-config.ts:64)裁决,输入是仓库的 SetupRunPolicy 和这次创建的 SetupDecision(两个类型都在 src/shared/orca-yaml-hook-types.ts:1-3):

decision(本次请求)policy(仓库配置)结果
run任意
skip任意不跑
inheritrun-by-default
inheritskip-by-default不跑
inheritask抛错:"Setup decision required for this repository"

最后一行是关键:ask 策略下,没有明确决定就是错误状态,而不是默认跑或默认不跑。渲染进程必须先问用户。

但这个错误分两处出现,处理方式完全相反:

  • 创建之前的预校验(src/main/ipc/worktree-remote.ts:2115-2118):让它抛,直接失败掉整次创建。
  • worktree 建好之后再读目标分支的 orca.yamlsrc/main/ipc/worktree-remote.ts:2603-2612):吞掉错误,只跳过 setup。注释说明理由 —— 目标分支可能新增了渲染进程从没收集过决定的 hook,而 worktree 已经建出来了,此时应该降级成"建好了但没跑 setup",而不是让整次创建失败。

顺带一提,目标分支的 orca.yaml 是权威的,Orca 不再要求它和主检出目录的内容一致 —— 因为"良性的差异悄悄禁用了 setup"是个真实 bug(src/main/ipc/worktree-remote.ts:2588 引用了 issue #1280)。

6.2 决策二:agent 等不等 setup

SetupAgentStartupPolicy 决定,只有两个值,默认 start-immediatelysrc/shared/setup-agent-startup-policy.ts:5):

policy行为适合什么项目
start-immediately(默认)setup 和 agent 并排启动依赖已装好、setup 只是刷新
wait-for-setupagent 等 setup 成功退出后才启动没装依赖 agent 根本没法工作

默认值选 start-immediately 的理由写在常量旁:存量仓库的行为不能被新功能改掉,要等必须显式选。

6.3 wait-for-setup 怎么实现:marker 文件

难点在于 setup 和 agent 跑在两个独立的 PTY 里,没有共享的进程关系。Orca 的解法是一个 marker 文件握手协议(createSequencedSetupAgentCommandssrc/shared/setup-agent-sequencing.ts:36)。

主进程 setup 终端 agent 终端
│ │ │
│ 生成 nonce + marker │ │
├──────────┬────────────┤ │
│ │ rm -f marker │
│ │ ( 跑 setup ) │
│ │ status=$? │
│ │ 写 tmp: "nonce:status" │
│ │ mv -f tmp → marker (原子) │
│ │ ┌───────────────┤
│ │ │ 循环: │
│ │ │ marker 存在? │
│ │ │ nonce 对得上?│
│ │ │ status==0 → exec 启动命令
│ │ │ status!=0 → 报错退出
│ │ │ 超时 2h → exit 124

四个设计点:

  • nonce 隔离并发。 marker 路径带一段随机 nonce(src/shared/setup-agent-sequencing.ts:44-56,nonce 进 marker 路径),否则同一个 setup runner 被并发启动两次会争抢同一个完成标记。
  • 先写临时文件再 mv POSIX 侧是 printf ... > tmp; mv -f tmp marker:86-87),保证读侧永远看不到半截内容。
  • 状态码要跟着传。 marker 内容是 nonce:status,等待侧据此区分"setup 成功了"和"setup 挂了",后者明确打印 Setup failed; skipping agent startup. 而不是照常启动 agent(:117)。
  • 有兜底超时。 默认 2 小时(:9),超时 exit 124(沿用 timeout(1) 的约定)。

Windows 侧是另一套 PowerShell 实现(:175:204),注释解释了为什么 setup runner 由 cmd.exe 启动、等待逻辑却用 PowerShell:PowerShell 能做有边界的文件轮询和正则解析,用 batch 的 label 循环写同样的东西太脆。

启动命令通过环境变量 ORCA_SEQUENCED_STARTUP_COMMAND 传给等待脚本(:10),而不是直接内联进 shell 字符串 —— 少一层引号地狱。

6.4 一条重要边界:主进程不执行 setup

createSetupRunnerScriptsrc/main/worktree-runner-script.ts:28)只做一件事:把 setup 脚本写成一个 runner 文件,落在该 worktree 自己的 gitdir 下(orca/setup-runner.sh.cmd)。执行由终端层负责。注释把这条界线说得很硬(src/main/ipc/worktree-remote.ts:2615 的 "main only writes the runner script and must not execute setup itself"):

// Why: main only writes the runner script and must not execute setup itself,
// or we reintroduce the old hidden background-hook behavior.

用户看得见的 setup 才是可调试的 setup。 而且 runner 生成失败只降级成"建好了但没启动 setup",不让整次创建失败(:2527-2529)。

runner 的 shell 选择跨了三种平台形态(底层 createWorktreeRunnerScriptsrc/main/worktree-runner-script.ts:68;shell 选择 resolveSetupRunnerShell:161):

worktree 位置runner 扩展名环境变量路径要不要转
POSIX(macOS/Linux).sh + chmod 755不转
原生 Windows + cmd.cmd不转
原生 Windows + Git Bash.shC:\.../c/...
WSL.shUNC 路径 → Linux 路径

6.5 真正的启动动作

spawnLocalStartupAndSetupTerminalssrc/main/ipc/worktree-remote.ts:265)是最后一步。它按顺序做:

  1. 如果需要顺序化,先把 setup 和 startup 两条命令包成上面那对握手脚本(:276-293)。
  2. TUI agent 的预信任标记(cursor / copilot / codex 各有各的写法,:297-310),best-effort,失败就让 agent 自己交互式地问。
  3. 起 agent 终端;失败就直接返回 warning,不再起 setup:327-332)。
  4. 起 setup 终端,位置由 setupScriptLaunchMode 决定 —— 新 tab 或上下/左右分屏(:347-367)。

创建函数上方那条注释点出了整段的时序前提(:296):

// Why: only after `git worktree add` + metadata registration is the path safe for
// a runtime PTY to boot the agent while setup runs alongside.

顺带一提,整条创建路径被 createWorktreeCreateTimingRecordersrc/main/worktree-create-timing.ts:35)逐段打点 —— refresh_base_refgit_worktree_addcreate_symlinkscopy_worktreeincludeprepare_setupspawn_startup_terminal 等,最终随结果返回。它的实现只有 66 行,且用 finally 记录,所以失败的阶段也有耗时数据。


7. 旁路:首条消息驱动的自动改名

7.1 它解决什么

创建时用户还不知道这个 workspace 要干什么,所以 Orca 给的是一个随机海洋生物名(you/Nautilus)。等 agent 收到第一条真实消息、开始干活,Orca 才有足够信息生成一个像样的分支名。

入口是 maybeAutoRenameBranchOnFirstWorksrc/main/agent-hooks/first-work-branch-rename.ts:95),主体在 runAutoRename:135)。

7.2 门禁清单

改一个已经存在的分支名是有风险的操作,所以门禁很长。每一关的失败都被明确分成两类stop(永久跳过,进 settled 缓存不再重试)和 retry(暂时性,下次事件再试):

关卡位置不过时
事件必须是真的"开始干活",不是重放:99直接返回
全局开关 autoRenameBranchFromWork:102直接返回
该 worktree 未在处理中、未 settled:112直接返回
当前有 checkout 的分支:182-185retry
必须是 Orca 建的、且不是"检出已有分支":186(判据在 src/main/index.ts:578stop
分支名必须还是自动生成的海洋生物名:191stop
分支必须还没有 upstream(没被推过):195-198stop
upstream 探测本身失败:199-207retry(曾经错判成 stop,issue #7808)

第五关的实现值得单看(src/main/index.ts:578-582):

// Why: a user branch could coincidentally match a creature name; only
// Orca-stamped worktrees are safe to auto-rename.
return !!meta?.orcaCreationSource && meta.preserveBranchOnDelete !== true

名字长得像不是证据,元数据里的创建来源才是。 并且 preserveBranchOnDelete(等价于"这是用户自己的分支,我只是检出了它")一票否决。

倒数第二关那条注释也很值得记:把"读不出来的探测"当成"有 upstream"来 settle,会让这个功能对该 worktree 永久且静默地失效 —— 因为 settled 缓存不会再重试。不确定 ≠ 否定。

7.3 生成之后要再验一遍

模型生成分支名要几秒。这几秒里分支可能被切走、被推送。所以生成完之后重复两项检查(:239-250):分支还是原来那个吗?还是没有 upstream 吗?任何一项变了就 retry,不做改名。

改名成功后还有三个收尾动作,每个都带条件:

  • 侧栏显示名只在它仍是自动生成名时同步(用户手打的名字不动,:284-290)。
  • 磁盘目录尽力对齐新的分支叶名,失败只记日志、绝不回滚已落地的分支改名(:295-303)。
  • 目录改名会自己通知渲染进程,所以只有它没发生时才补一次 onRenamed:305-308)。

folder workspace 没有分支可改,走的是并行分支 runFolderWorkspaceTitleAutoRenamesrc/main/agent-hooks/first-work-workspace-title-rename.ts:14)—— 复用同一套生成机器和同一套 stop/retry 语义,只是落点从分支变成标题。


8. 认领:外部 worktree 和所有权判定

删除路径的防御全部建立在一个问题上:这个 worktree 到底是不是 Orca 建的? 这个判定单独抽在 src/shared/worktree-ownership.ts

8.1 四类所有权

classifyWorktreeOwnershipsrc/shared/worktree/ownership.ts:111按顺序判,命中即停:

顺序类别判据
1orca-managed元数据里有强证据(见下)
2agent-scratch路径匹配 agent 的临时工作树(如 .claude/worktrees
3unknown-legacy落在 Orca 的扁平(非嵌套)workspace 根下
4external落在 Orca 的嵌套 workspace 根下,或完全在外面

"强证据"是 hasStrongOrcaMetadata:253)—— orcaCreatedAtorcaCreationWorkspaceLayoutcreatedAtcreatedWithAgentpushTargetsparseBaseRefsparsePresetIdpreserveBranchOnDelete 任一存在即可。这些字段合起来构成"这条记录是 Orca 写的"的判据。

第 3、4 条的区别是路径歧义:扁平布局下,Orca 的 workspace 根和用户随手 git worktree add 的目标可能混在一起,分不清就归到保守的 unknown-legacy;嵌套布局下路径结构更强,才敢判 external。注释说得很直接(:165-167):

// Why: a plain `git worktree add` can target Orca's nested workspace folder.
// Only metadata proves Orca created it.

8.2 可见性是另一个问题

分类完还要决定"要不要在侧栏显示"(shouldShowWorktree:203)。规则同样是顺序判定:当前选中的检出永远显示 → Orca 管理的永远显示 → 显式导入过的外部 worktree 显示 → agent-scratch 永远不显示(除非前两条命中)→ 老仓库的 unknown-legacy 显示 → 其余看仓库的外部可见性设置。

agent-scratch 单独一条的理由写在旁边:它是工具管道,不是工作区,即使仓库开了"显示外部 worktree"也该藏着(issue #9388)。

8.3 扫描缓存

列举检测到的 worktree 要跑 git worktree list,渲染进程会轮询。listDetectedGitWorktreessrc/main/ipc/worktrees.ts:701)加了 5 秒 TTL 缓存 + in-flight 合并(:568),注释说明取舍是"吸收轮询突发,同时把外部变更的延迟限制在一个短刷新窗口内"。

其中一处并发正确性值得看(:658-664):如果扫描进行中被 create/remove 通知打断(invalidate),这次扫描的结果不允许再写回缓存。靠的是给每个 in-flight 扫描挂一个 invalidated 标志位,而不是简单地删 map。


9. 死亡:为什么删除路径要写这么长

9.1 一句话概括风险

删除的最终动作可能是递归删一个目录。判错的代价是删掉用户的代码。所以整条路径的设计原则只有一条:没有证明,不递归删。

9.2 先看分诊

worktrees:removesrc/main/ipc/worktrees.ts:2474)拿到请求后,第一件事是重新列 git 的注册表,然后按"这条路径在 git 眼里是什么状态"分诊:

收到删除请求

┌──────────┴──────────┐
│ 并发去重:同 key 同参数 → 复用 promise
│ 同 key 不同参数 → 拒绝
└──────────┬──────────┘

git 还注册着它吗?
│ │
是 │ │ 否
▼ ▼
┌─────────────┐ ┌──────────────────────────────┐
│ 正常删除路径 │ │ A. .git 文件双向证明成立 │
│ (§9.4) │ │ → 要 force → 递归删 │
└─────────────┘ │ B. .git 已丢但元数据证明是 │
│ Orca 建的 → 要 force → 删 │
│ C. 目录本来就没了 → 只清元数据 │
│ D. 都不成立 → 拒绝 │
└──────────────────────────────┘

并发去重src/main/ipc/worktrees.ts:2472:2260-2266):过期 toast、双击、侧栏同时触发删除是常见的。同一个 worktree + 同一组参数复用同一个 promise;参数不同(比如一个 force 一个不 force)则直接拒绝,绝不并行跑两条会互相踩的路径。

9.3 A 分支的证明:孤儿 gitdir 双向验证

最有意思的是 A 分支的判据 —— canSafelyRemoveOrphanedWorktreeDirectorysrc/main/worktree-removal-safety.ts:154)委托给 gitFileProvesOrphanedWorktreeDirectorysrc/main/worktree-orphan-gitdir-proof.ts:7)。

Git 的 linked worktree 天然是一对互指的引用,这个证明就是把两个方向都验一遍:

worktree 目录 仓库的 .git/worktrees/
┌──────────────────┐ ┌──────────────────────┐
│ <wt>/.git (文件)│ ──① gitdir:──▶ │ .../worktrees/<name>/ │
│ │ │ │
│ <wt>/.git │ ◀──② gitdir ── │ .../<name>/gitdir │
└──────────────────┘ └──────────────────────┘
两个方向都对得上,才算证明成立

三层校验缺一不可:

  1. <wt>/.git 必须是文件不是目录(:17),文件才是 linked worktree 的形态。
  2. 它指向的 gitdir 必须恰好是仓库 worktrees 目录的直接子项 —— 相对路径不能为空、不能再含分隔符(:150-153)。
  3. 反向的 <gitdir>/gitdir 必须指回 <wt>/.gitadminGitdirPointsAtCandidate:157)。注释点明了这一步防的是什么:
// Why: copied .git files can target another worktree's admin entry; only
// Git's back-reference proves that entry still belongs to this candidate.

一个被复制过来的 .git 文件可以指向别人的 admin 目录,只有反向引用能排除这种伪装。

resolveRepoWorktreesPath:74)还处理了一个套娃情况:仓库本身可能就是一个 linked worktree。它靠 admin 反链来确认,注释说明"名叫 worktrees 的目录到处都能建,只有 admin 反链能证明这个仓库路径本身是 linked worktree"。

B 分支(canCleanupUnregisteredOrcaLeftoverDirectorysrc/main/worktree-removal-safety.ts:192)是 .git 标记已经没了的恢复态。此时上面那套证明用不了,所以它改用另一套证据并把理由写在最前面:

// Why: without a surviving .git file, path shape alone is too weak to prove
// ownership for recursive deletion; require persisted Orca-created evidence.

路径形状不是权威 —— 用户完全可以在 Orca 的 workspace 目录里手工 git worktree add:184-186)。

9.4 五道防线

不管走哪条分支,下面这些检查是删除路径的骨架:

#防线位置拦什么
1危险路径黑名单src/main/worktree-removal-safety.ts:65空路径、等于仓库路径、文件系统根、包含仓库路径、包含 home、/home/x /Users/x 形状
2必须在 git 注册表里:115 findRegisteredDeletableWorktree未注册的路径;主 worktree 直接拒
3不能内含另一个注册的 worktree:131remove --force 会把嵌套 worktree 当普通未跟踪目录删掉,只留一条可 prune 的孤儿记录
4git 锁src/shared/worktree/removal.ts:3LOCKED_WORKTREE_REMOVAL_PREFIXgit worktree lock 锁住的,force 也不行,必须显式 unlock
5工作区干净assertWorktreeCleanForRemoval有改动就要求 force

第 1 条里"包含仓库路径"这个方向容易看反 —— 检查的是"要删的目录是不是仓库的祖先",防的是删一个把整个仓库套在里面的目录。

第 4 条的语义边界写在 classifyWorktreeForceDeleteReasonsrc/shared/worktree/removal.ts:89-107classifyWorktreeForceDeleteReason):git 锁不算可以用 force 覆盖的原因,因为"锁可能代表一份外部的安全约定"。Orca 的 force 只覆盖"有未提交改动"这一种。

9.5 归位时的两次重查

第 4、5 道防线在正常路径上其实查了两遍。原因是中间夹着 archive hook(用户的清理脚本),它跑的时候 worktree 目录还完整,但它可能跑很久,期间另一个 git 客户端可以锁住这一行。

所以 hook 跑完之后,Orca 重新列一次注册表、重新定位、重新验锁(src/main/ipc/worktrees.ts:2955-2975):

// Why: an archive hook can race another Git client that locks the row;
// recheck before linked-path/watcher/terminal teardown.

如果这次找不到了,直接抛 Worktree registration changed during deletion: ... Retry deletion. —— 状态变了就重来,不猜。

9.6 删除时的闸门顺序

真正动手时有一套固定顺序(src/main/ipc/worktrees.ts:3006-3023):

acquireFileWatcherRemoval ← 拿闸门(挡住新的 watcher/终端注册)

├─ stopPtysForDestructiveWorktreeRemoval ← 杀光该 worktree 的所有 PTY
│ (requirePhysicalStop: true —— 必须物理停止,不接受逻辑注销)

├─ removeWorktreeLinkedPaths ← 拆软链

├─ git worktree remove ← 交给 git
│ └─ 失败 → 平台恢复 / 孤儿回退(见 §9.7)

removalGate.finish(completed) ← finally 里释放,成败都释放

注释解释了为什么闸门要罩住整段而不只是 git 那一步(:2689-2690):递归回退删除同样是破坏性的,而 Windows / WSL 上只要还有进程持着原生句柄,删除就会失败。先清光句柄,再删。

软链要在 git 之前拆掉,原因写在 removeWorktreeSymlinks 的注释里(src/main/ipc/worktree-symlinks.ts:388-399):指向主目录 node_modules 的软链在 git 眼里是"未跟踪文件",不先拆,用户每次删除都会撞上"It has changed files. Use Force Delete"。

9.7 失败之后的两条恢复路

git worktree remove 抛错了怎么办?分两种。

① Windows 半死状态。 Git for Windows 会先注销 worktree、再递归删目录,第二步可能瞬时失败 —— 于是 git 认为删完了,磁盘上还留着目录。recoverLocalWindowsWorktreeRemovalsrc/main/local-worktree-removal-recovery.ts:111)处理它,判据非常克制(:134-150):

// Why: error prose can be localized or ambiguous. Only a missing Git row
// proves removal started and makes recursive Windows cleanup safe.

不读错误文本(会被本地化),而是重新列一次注册表,只有那一行确实消失了才认为"删除已经开始",才敢补做递归删除。而且递归删之前还要先 closeWatcher,理由同样是"必须 fail-closed,只要可能还有 watcher 持着原生句柄就不动手"。

② git 说这是孤儿。 回到 §9.3 那套双向证明再验一次;证明不成立就打日志拒绝(src/main/ipc/worktrees.ts:3077-3081):

Refusing recursive cleanup for unproven worktree directory: ...

不能删就宁可留着,绝不猜。 然后补一次 git worktree prune:2757-2761),注释指出原因:remove 失败说明 git 还留着 .git/worktrees/<name>,不 prune 的话这条陈旧记录会一直锁着那个分支。

9.8 分支的命运

删 worktree 顺带删分支,但绝不能悄悄丢掉提交deleteBranchAfterWorktreeRemovalsrc/main/git/worktree.ts:1294)的策略是:

步骤动作说明
1git branch -d(不是 -D-d 拒绝删未合并分支,正是想要的保护
2报"分支被某 worktree 检出"补一次 worktree prune 再试一次(:1235-1241
3-d 因未合并而失败尝试"已合并判定":squash merge 会重写 commit id,-d 认不出来
4仍然判不出已合并保留分支,返回 preservedBranch { branchName, head }

第 4 步返回的 preservedBranch 会被主进程记进 preservedBranchCleanupByScopesrc/main/ipc/worktrees.ts:567,键从 worktreeId 改为 scope),UI 可以据此提供一个"确认要连分支一起删"的显式动作,走 worktrees:forceDeletePreservedBranch:2913)。那个通道会核对 branchName expectedHead 都一致才执行(:550-560)—— 也就是说,这个二次确认是绑定到具体那个 commit 的,分支在此期间动过就作废。

记录时还有一条硬约束(:519-523):没有 head 就抛错,因为"没有保存的 commit,就没法安全地提供强制删除"。

forceBranchDelete: true(即 -D)只在一个场景用:创建失败的回滚 —— 那是一个刚建出来、用户还没写过任何东西的分支(src/main/git/worktree.ts:1152 的注释)。

SSH 侧的 removeWorktreeOpsrc/relay/git-handler-worktree-remove.ts:125)镜像了同一套策略,包括同样用 -d 不用 -D:189)。它额外处理了一个 git 行为:只要 worktree 里有初始化过的 submodule,git 就拒绝非 force 删除,哪怕一切都是干净的。relay 的做法是重新自证干净(父仓库的 status 会把脏 submodule 报成 M <sub>),确认干净后才 --force:163-175)。本地侧有同样的处理(src/main/git/worktree.ts:1194-1200)。

9.9 forgetLocal:不碰远端的退出通道

如果一个 workspace 挂在一个已经死掉的 SSH 目标上,worktrees:remove 必然抛错,用户就被卡住了。worktrees:forgetLocalsrc/main/ipc/worktrees.ts:3170)是逃生口:只杀进程、清元数据,不做任何 git 或远端文件操作。

它和 remove 共享同一个 in-flight key(:2837-2843),所以两者不会并发。它也复用同一个 folder 根保护 —— 项目根 workspace 不允许被删(:2855-2861)。

元数据清理统一走 removeWorktreeMetadataAndTransientState:305),一次性清掉五样进程内状态:worktree meta、advertised URL 监听、localhost 标签路由、终端历史目录(异步删,绝不在删除关键路径上做递归 rmSync)、PR 刷新别名。理由还是那句 —— id 是路径派生的、会被复用


10. git 版本兼容:怎么在不知道对方 git 多老的情况下干活

Orca 跑的是用户机器上的 git,而且可能同时是原生、WSL、SSH 三种主机,版本各不相同。基线定在 git 2.25。

策略不是 git --version 判断,而是先试新命令,按错误特征降级,并把结果缓存到该主机。特征识别函数集中在 src/shared/git-worktree-command-capabilities.ts

函数识别什么判据
isUnsupportedWorktreeListZError:17worktree list -z 不支持(git < 2.36)退出码 129 是与语言无关的信号;正则匹配是补充
isUnsupportedRevParsePathFormatError:26rev-parse --path-format 不支持错误文本正则
hasUnsupportedRevParsePathFormatEcho:32老 git 回显未知选项且退出码为 0输出里有行以 --path-format 开头

第三个是最容易踩的坑:某些老 git 不报错,而是把不认识的选项原样打回来并正常退出。解析代码能恢复,但能力探测必须记住这个信号(src/main/git/worktree.ts:421 的注释与后续处理):

// Why: some old Git echoes the unknown option and exits zero; remember that
// compat signal even though parsing recovers.
capabilities.rememberUnsupported('rev-parse-path-format')

-z 降级还带来一个次生问题:-z 才有的 porcelain 输出里,git < 2.31 没有 prunable 标注。于是降级分支要自己用 stat 逐个探测路径是否存在来补这个标注(src/main/git/worktree.ts:651annotatePrunableByExistence(调用点 :644),relay 侧对应 src/relay/git-handler-worktree-list.ts:42)。不补的话,一条陈旧注册会被当成活的 workspace 显示出来(issue #8389)。

这个探测还有一个刻意的例外:被锁的 worktree 即使目录不存在也不标 prunable —— 锁在保护这条注册记录,Orca 不越权。


11. 巧妙之处(可以带走的)

① 把"证明"和"动作"彻底分开。 所有权判定(src/shared/worktree-ownership.ts)、危险路径判定(src/main/worktree-removal-safety.ts)、孤儿 gitdir 证明(src/main/worktree-orphan-gitdir-proof.ts)全是纯函数 + 注入的 stat/read,所以本地和 SSH 用同一份逻辑,测试也不需要真实文件系统。破坏性动作只在 IPC 层,且必须先拿到这些函数的 true

② 不确定要区分成"永久不行"和"暂时不行"。 自动改名的 stop / retry 双语义(src/main/agent-hooks/first-work-branch-rename.ts:143-153)就是为了这个 —— 把探测失败错判成 stop,会让功能永久且静默地失效(issue #7808)。

③ 错误文本不可信,状态可信。 Windows 删除恢复不读 git 的错误文本(会被本地化),而是重新列注册表看那一行还在不在(src/main/local-worktree-removal-recovery.ts:145-148)。-z 不支持的判据首选退出码 129 而不是英文正则,同一个道理。

④ 先量后拷,因为拷了就停不下来。 fs.cp 忽略 signal,所以复制预算必须在写第一个字节之前给出否决(src/main/ipc/worktree-include-copy-budget.ts:125-129)。连"丈量"本身都有独立限额,并且超支归因要如实(sizing vs entries)。

⑤ 路径派生的 id 必须配一个轮换的 instance id。 同一路径删了又建,id 完全相同;instanceId: randomUUID()src/main/ipc/worktree-remote.ts:2482)是唯一能把"上一任"的血缘和会话挡在门外的东西。

⑥ 两个 PTY 之间的同步,用带 nonce 的 marker 文件。 不需要共享进程树、不需要额外的 IPC 通道,printf > tmp; mv -f tmp marker 就能得到原子发布,nonce 解决并发(src/shared/setup-agent-sequencing.ts:36)。

⑦ 预热昂贵的副作用。 worktrees:prefetchCreateBase 把 fetch 提前到用户还在填表时,prepareLocalWorktreeRootForReposrc/main/worktree-root-preparation.ts:14)把 macOS TCC 权限弹窗提前到启动时。两者都吞掉自己的错误,因为它们只是优化。

⑧ 慢操作要按碰撞概率放。 gh pr list 只在撞名之后(suffix > 1)才查(src/main/ipc/worktree-remote.ts:2248-2249)。绝大多数创建路径根本不付这个 1–3 秒。


12. 边界与局限

  • worktree id 是路径派生的,所以磁盘上改目录名等于换 id。Orca 靠 migrateWorktreeIdentity 和历史 id 列表来缝合,但这是补救,不是设计上的干净。
  • 孤儿目录的清理需要 force 非 force 时抛的是 ORPHANED_WORKTREE_DIRECTORY_MESSAGEsrc/main/worktree-removal-safety.ts:43),必须由 UI 转成一次显式确认。想"一键清干净"的用户会觉得绕。
  • .git 标记丢失后的恢复完全依赖持久化元数据。 如果用户的 Orca 配置也丢了,那个目录就再也无法被自动清理 —— 这是刻意的失败方向(src/main/worktree-removal-safety.ts:203-208)。
  • APFS clone 只在 macOS 生效。 Linux / Windows 上 .worktreeinclude 是真复制,2 GB 预算会先撞上(src/main/ipc/worktree-include-copy-budget.ts:12-17)。
  • ask 策略下缺少 setup 决定,创建前会直接失败。 这是刻意的,但意味着任何绕过渲染进程的自动化路径都必须显式带上 setupDecision
  • 自动改名依赖分支名"看起来是自动生成的"。 用户如果手工把分支改回一个海洋生物名,加上有 Orca 创建元数据,它仍会被改名。这一条源码里没有额外防护。
  • git worktree lock 是绝对边界。 Orca 的 force 覆盖不了它(src/shared/worktree/removal.ts:96-98 的注释),用户必须自己 git worktree unlock

13. 代码地图

主题文件路径关键符号
创建/删除 IPC 入口src/main/ipc/worktrees.tsregisterWorktreeHandlersworktrees:createworktrees:removeworktrees:forgetLocal
本地创建主流程src/main/ipc/worktree-remote.tscreateLocalWorktreecreateRemoteWorktreespawnLocalStartupAndSetupTerminals
base ref 三级优先级src/main/worktree-create-base.tsresolveWorktreeCreateBase
base 预热src/main/worktree-create-base-prefetch.tsprefetchWorktreeCreateBasehasLocalWorktreeBaseRef
默认 base 探测顺序src/main/git/repo.tsDEFAULT_BASE_REF_PROBESresolveDefaultBaseRefViaExecgetBranchConflictKind
fetch 合并与新鲜度缓存src/main/runtime/orca-runtime.tsgetOrStartRemoteFetchgetOrStartRemoteTrackingBaseRefreshresolveRemoteTrackingBase
后缀候选名src/main/worktree-create-candidates.tsWORKTREE_CREATE_MAX_SUFFIX_ATTEMPTSgetWorktreeCreateCandidate
分支名前缀src/main/ipc/worktree-branch-name.tscomputeValidatedBranchNamegetConfiguredBranchPrefix
名字/路径清洗与围栏src/main/ipc/worktree-logic.tssanitizeWorktreeNameensurePathWithinWorkspacecomputeWorktreePathcomputeWorkspaceRoot
workspace 根预建(TCC 预热)src/main/worktree-root-preparation.tsprepareLocalWorktreeRootForRepo
本地 git worktree 读写src/main/git/worktree.tsaddWorktreeaddSparseWorktreeremoveWorktreedeleteBranchAfterWorktreeRemovallistWorktreesStrict
base ref 消歧src/shared/worktree/base-ref.tsresolveWorktreeAddBaseRef
创建耗时打点src/main/worktree-create-timing.tscreateWorktreeCreateTimingRecorder
软链/共享/复制物化src/main/ipc/worktree-symlinks.tsmaterializeWorktreePathscreateWorktreeLinkedPathscreateWorktreeCopiedPathscreateWorktreeSharedPathsremoveWorktreeLinkedPaths
APFS clonefilesrc/main/ipc/worktree-apfs-clone.tscloneWorktreePathWithApfscanCloneWithApfsDarwinFilesystemCache
复制预算src/main/ipc/worktree-include-copy-budget.tscreateWorktreeCopyBudgetTrackerDEFAULT_WORKTREE_COPY_BUDGETformatWorktreeIncludeCopyWarning
setup 跑不跑src/main/hooks.tsshouldRunSetupForCreategetEffectiveSetupRunPolicycreateSetupRunnerScriptcreateWorktreeRunnerScript
setup 与 agent 时序src/shared/setup-agent-sequencing.tscreateSequencedSetupAgentCommandsSETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV
启动策略常量src/shared/setup-agent-startup-policy.tsDEFAULT_SETUP_AGENT_STARTUP_POLICYshouldWaitForSetupBeforeAgentStartup
runner 命令构造src/shared/setup-runner-command.tsresolveSetupRunnerCommandnativeWindowsPathToPosixShellPath
首条消息改名src/main/agent-hooks/first-work-branch-rename.tsmaybeAutoRenameBranchOnFirstWorkrunAutoRename
folder workspace 改标题src/main/agent-hooks/first-work-workspace-title-rename.tsrunFolderWorkspaceTitleAutoRename
改名生成目标src/main/agent-hooks/first-work-generation-target.tsresolveGenerationTarget
所有权与可见性src/shared/worktree-ownership.tsclassifyWorktreeOwnershipshouldShowWorktreebuildKnownOrcaWorkspaceLayouts
删除安全判定src/main/worktree-removal-safety.tsisDangerousWorktreeRemovalPathfindRegisteredDeletableWorktreecanSafelyRemoveOrphanedWorktreeDirectorycanCleanupUnregisteredOrcaLeftoverDirectory
孤儿 gitdir 双向证明src/main/worktree-orphan-gitdir-proof.tsgitFileProvesOrphanedWorktreeDirectoryadminGitdirPointsAtCandidate
锁与 force 语义src/shared/worktree/removal.tsassertWorktreeUnlockedForRemovalclassifyWorktreeForceDeleteReason
Windows 删除恢复src/main/local-worktree-removal-recovery.tsrecoverLocalWindowsWorktreeRemovalremoveStaleLocalWorktreeRegistrationAfterFilesystemRemoval
git 版本能力探测src/shared/git-worktree-command-capabilities.tsisUnsupportedWorktreeListZErrorhasUnsupportedRevParsePathFormatEcho
relay 侧 worktree 操作src/relay/git-handler-worktree-ops.tsaddWorktreeOpworktreeIsCleanOp
relay 侧删除src/relay/git-handler-worktree-remove.tsremoveWorktreeOpdeleteRelayBranchAfterWorktreeRemoval
relay 侧列举src/relay/git-handler-worktree-list.tsreadRelayWorktreeListannotatePrunableWorktreesByExistence

接着读: worktree 建好之后,终端和守护进程怎么把它跑起来 → 终端底座;Orca 怎么认出里面跑的是哪个 agent、状态如何回传 → 认得出 agent;同一套创建/删除逻辑如何被 RPC 内核和远程客户端复用 → 一个 runtime,多种客户端