在 IntelliJ Platform 仓库中使用 Treehouse 工作区租约生命周期:从 Skill 指南到 Go 包装器实现解析
【免费下载链接】intellij-communityIntelliJ IDEA & IntelliJ Platform项目地址: https://gitcode.com/GitHub_Trending/in/intellij-community
本篇技术指南系统讲解 IntelliJ Platform(intellij-community)源码仓库中为 AI Agent 隔离工作区而设计的Treehouse 工作区租约生命周期:涵盖 Skill 文档规定的read status/write acquire/write return三个受控命令、租约收据(receipt)的 schema 约定、错误码语义,以及 build/treehouse 目录下完整 Go 包装器的源码级实现原理。读完你将掌握在大型 monorepo 中安全地获取、使用、归还隔离工作区的完整流程,并理解其"绝不越权、绝不绕过、以实时租约状态为准"的安全设计。
背景:为什么大型 monorepo 需要受控的工作区隔离
IntelliJ Platform 仓库体量极大,Agent 在任务执行期间临时创建 Git worktree 或额外 clone 来隔离工作区,成本高且容易失控。因此仓库在 .ai/workspace-isolation.md 中明确了统一策略:
- 需要隔离工作区时,必须使用
treehouseskill(即本指南对应的 .claude/skills/treehouse/SKILL.md),它承载了完整操作流程; - 禁止绕过该 skill 的包装器(wrapper)直接调用裸的 Treehouse 生命周期命令,也禁止运行
treehouse enter、init、update、prune、destroy或--force——因为这些命令可能进入、修改甚至删除其他会话的工作区; - 禁止在未获明确指示时自行
git worktree add、clone 仓库或实现自定义隔离机制;唯一例外是用户显式要求为当前任务创建 Git worktree 时,可创建恰好一个、仅限定于该任务的工作区; - 任何 Treehouse 命令失败时,不得擅自回退到 worktree、clone 或其他工作区管理器;若当前 checkout 下继续操作是安全的,就直接继续,否则请用户提供隔离工作区。
这个策略文档是理解后面所有命令与源码的前提:一切设计都围绕"受控、可审计、不碰别人会话"展开。
Skill 全貌:frontmatter 与受控命令面
.claude/skills/treehouse/SKILL.md 是 Claude Code 的 skill 定义文件,其 frontmatter 声明了元信息:
--- name: treehouse description: Safely acquire, inspect, and return leased Treehouse workspaces. allowed-tools: Bash(../../../community/tools/treehouse.cmd read:*), ... ---注意文件头部的一行注释:Generated by community/.ai/render-guides.mjs; edit community/.agents/skills/treehouse/SKILL.md。也就是说,这份 SKILL.md 是由 .ai/render-guides.mjs 渲染脚本从规范源(community 版源码树中的.agents/skills/treehouse/SKILL.md)生成的,同时按工具(Claude / Codex / Junie)分发到.claude、.codex、.junie等目录。allowed-tools中的每个条目都是一个 Bash 前缀授权,只允许以read:或write:开头的包装器调用,从权限层面就把命令面锁死。
包装器只暴露 Treehouse 的租约生命周期三命令,其他能力一概不暴露:
| 命令 | 作用 |
|---|---|
read status | 查看资源池中所有工作区及其租约、进程列表(只读) |
write acquire | 获取一个带租约的工作区并做 HEAD 对齐准备 |
write return | 归还租约并清理工作区 |
它从不安装Treehouse,也不暴露enter、init、update、prune、destroy与--force。这一点在 build/treehouse/main.go 的包注释中写得很明确:It never installs Treehouse, never creates an ad hoc Git worktree, and never passes --force to the Treehouse CLI.
运行 CLI:两种 checkout 的命令拼写与输出格式
Skill 文档要求从仓库根目录运行 CLI:
- Ultimate checkout(monorepo)中命令拼写为
./community/tools/treehouse.cmd; - Community checkout 中拼写为
./tools/treehouse.cmd(去掉community/前缀)。
由于 Bazel 从固定的(pinned)源码构建 CLI,会话中第一次调用会明显更慢;命令输出为 JSON。文档特别提醒:把一个 read 调用和一个 write 调用分开运行,这样审批(approval)可以保持窄作用域且可复用——例如只授权read:前缀的 Bash 规则,就永远无法触发write操作。
从实现上看,这个"两段式语法"是刻意设计的。看 build/treehouse/main.go 的参数解析:命令文法只取两个 token,第一个是访问词(read/write),第二个是动作词(status/acquire/return)。这样一条 Bash 审批可以按前缀作用域授权:批准read不会连带批准write(见同文件包注释:"An approval ofreadcannot authorize a write.")。
成功时命令在 stdout 输出{"ok":true,"data":...},失败时在 stderr 输出{"ok":false,"error":...,"details":...}并设置退出码。
查看资源池:read status
./community/tools/treehouse.cmd read status结果列出每一个工作区,包含其租约状态与进程列表。status只做检查:一个可用工作区可能显示为旧的 detachedHEAD(因为它没有被 prepare)。文档明确警告:绝不要对status返回的路径执行 enter、edit、reset、rebase 或 synchronize——只有write acquire才负责保留并准备工作区。
源码层面,read status在 status.go 中实现:它调用上游 CLI 的treehouse status --json,然后经parseStatus严格校验每条记录的name、path、status字符串字段以及lease_id、lease_holder、processes的类型(status.go)。WorkspaceStatus特意保持为map[string]any,这样未知字段(如flavor)能原样往返,不会在转发中丢失。测试 treehouse_test.go 验证了 status 输出的结构化字段与底层treehouse status --json的调用关系。
获取工作区:write acquire
./community/tools/treehouse.cmd write acquire --holder <session-id>持有者(holder)解析顺序:优先使用--holder传入的当前开发/Agent 会话 ID;未传时读取环境变量TREEHOUSE_LEASE_HOLDER;再不行则自动生成agent-<UUID>标签。这正好对应 acquire.go 中holderFrom的实现逻辑。结果返回:工作区路径(path)、租约 ID(lease_id)、持有者(lease_holder)与收据路径(receipt_path)。
acquire 的语义(来自文档 + acquire.go 的executeAcquire):
- 以
--no-fetch方式取得一个干净租约,并在调用方当前的精确 HEAD上做 detached checkout; - 不转移调用方的 index、工作树改动或 untracked 文件,也不执行 fetch、rebase、stash、cherry-pick 或文件拷贝;
- 仅当当前 checkout 本身已持有租约时拒绝——因此一个 checkout 可以同时持有多个租约;
- acquire 前置检查包括:当前工作区是否已有活跃租约(有则退出码 2 拒绝,见 treehouse_test.go)、目标路径必须是 Git 工作区根、且与源仓库共享同一个 Git common dir(用
git rev-parse --git-common-dir校验,见 git.go)。
HEAD 对齐(prepare)细节:prepareAcquiredWorkspace(acquire.go)先确认实时租约与分配结果一致,然后检查目标工作区干净,若 HEAD 与源 HEAD 不同则执行git checkout --force --detach <source-head>(git.go)。这个--force是Git的 force 而非 Treehouse CLI 的--force:因为大小写不敏感文件系统会把仅大小写改名的重命名藏出git status的视野,导致普通 checkout 被 Git 拒绝。安全性由前后两次校验兜底——detach 前gitChanges拒绝脏工作区,detach 后再读 HEAD 并检查 changes,任何一项与源 HEAD 不符即失败。测试 treehouse_test.go 专门锁定了"detach 带--force但 Treehouse CLI 永远收不到--force"这条规则。
租约收据(receipt):acquire 成功后,工作区内写入out/treehouse/lease.json,schema 版本为2,记录捕获的source_head。两种仓库布局都会忽略out/目录。收据字段定义在 receipt.go:schema_version、path、lease_id、lease_holder、acquired_at、source_head。文档要求:不要编辑、移动或复制收据。只有 schema 版本 2 的收据会被接受——测试 treehouse_test.go 验证了旧版 v1 收据(来自已退役的 Bun 脚本)在任何 spawn 之前就被拒绝。
归还工作区:write return
归还前必须确认:所有预期改动都已提交或保存在别处,且工作区内没有预期遗留的未提交/未跟踪工作。接着停止read status报告的所有进程——包装器在 Treehouse 仍报告有进程时会拒绝归还。然后从原始 checkout 或租约工作区之外的另一个目录运行(从外部运行可以避免包装器及其父 shell 出现在该工作区的进程列表里)。
./community/tools/treehouse.cmd write return --workspace <leased-path>归还的校验链(returncmd.go 的executeReturn):
- 拒绝从租约工作区内部运行(防止包装器自身变成"活进程");
- 读取并解析工作区内收据(schema v2),且收据中的
path必须与请求路径一致; - 校验路径是 Git 工作区根;
readStatus拿实时租约,收据的lease_id+lease_holder必须与实时状态完全匹配(双身份守卫);- 有活进程则拒绝;
- 工作区脏(
git status --porcelain=v1 --untracked-files=all有输出)且未带--confirm-preserved时拒绝; - 调用
treehouse return <path> --if-lease-id <id> --if-lease-holder <holder>(见 receipt.go 的returnCommand); verifyReturned再次查询实时池确认租约已消失,以实时租约状态为准,而非退出码(returncmd.go 及 status.go 的confirmLeaseGone);- 只有确认成功后,才删除收据。
脏工作区的确认标志:
./community/tools/treehouse.cmd write return --workspace <leased-path> --confirm-preserved该标志直接回答 Treehouse 的Clean and return? [Y/n]提示,因此无需 TTY。它只能在上面所有检查通过后使用——因为归还动作会清空工作区。包装器对脏归还拒绝且绝不替用户代劳--force。
实现上,returnPromptAnswer常量(receipt.go)就是"y\n":当工作区脏时,包装器通过 stdin 写入这个回答来回应 CLI 的确认提示。测试 treehouse_test.go 明确验证:脏归还无需 TTY,--confirm-preserved就是那个回答本身。
命令失败时的处理:退出码 2 与 127 的语义
失败时 CLI 在 stderr 输出 JSON 失败文档,包含消息(message)、退出码与细节(details)。两种退出码语义必须区分(见 main.go 的nativeFailure):
- 退出码 2:用法错误或前置条件失败(如缺少
--workspace、收据与实时租约不匹配、存在活进程、工作区脏而未确认等); - 退出码 127:固定的 CLI 二进制未能解析。这是 Bazel 构建失败或 runfiles 失败,而不是宿主机缺少安装。对应的错误消息明确指示不要安装 Treehouse。
此外,包装器会把子进程退出码钳制到进程可报告的 1–255 范围(main.go),native_exit_code细节字段保留未钳制的原始值。
失败后的纪律(文档原话要求):
- 绝不安装 Treehouse;
- 绝不擅自回退到 Git worktree、clone 或其他工作区管理器;
- 当前 checkout 下继续操作安全时就继续,否则请用户提供隔离工作区;
- 只有用户明确要求时,才使用 Git worktree;
- 若租约在失败后仍然存活,保留它,并从错误信息中报告路径、租约 ID 与持有者。
一个值得注意的设计:acquire 过程自带回滚。当收据写入失败、HEAD 准备失败、实时身份变化或上游输出畸形时,rollbackAcquire(acquire.go)会主动归还刚拿到的租约,并再次以实时池确认;确认失败或无法读取时,保留收据并输出"retain this lease identity"(保留此租约身份)的指引。相关分支在 treehouse_test.go 中有 8 组以上测试覆盖。
Codex 沙箱下的使用流程
Skill 文档最后一节专门针对 Codex 环境给出了 4 步操作纪律:
- acquire 前先检查内置的
request_permissions工具是否可用;不可用则不要获取租约,并报告本会话无法使用 Treehouse——不得要求用户修改权限设置、以--add-dir重启或授予 Treehouse 池访问权; - 从本 skill 目录运行,并请求两个审批前缀:
../../../community/tools/treehouse.cmd read与../../../community/tools/treehouse.cmd write(Community checkout 中去掉community/)。write 审批只让包装器触达 Treehouse 池,并不授权acquire 或 return 本身; - 工具的 cwd 变更不会把租约工作区加入会话可写根。保持从原始 checkout 运行,用
request_permissions为恰好返回的那个 path申请会话级写访问(在任何编辑之前)。不要申请 Treehouse 池、源码 checkout、共享 Git 目录或全量访问,也不要改用逐命令提权;授权后,后续工具一律以该工作区路径为工作目录; - 授权被拒时,不得进入、编辑或在租约工作区中运行命令,立即从原始 checkout 归还未触碰的租约,也不要求用户重新配置权限。
这套流程与"read/write 分开审批""从外部归还"的设计一脉相承:所有提权都收窄到最小必要范围。
源码级实现:Go 包装器的整体设计
包装器源码集中在 build/treehouse,模块为jetbrains.com/treehouse,只用标准库;上游 Treehouse 模块通过独立二进制//build/treehouse/cli到达,绝不作为 import 依赖(见 BUILD.bazel 注释)。关键设计点:
- pinned CLI 与 runfiles 解析:runtime.go 的
treehouseCLIPath从不搜索 PATH,因此宿主机上安装的 Treehouse 无法顶替固定版本。解析顺序:TREEHOUSE_CLI_BIN环境变量覆盖 → Bazel 注入的 rlocation 路径(经RUNFILES_DIR、RUNFILES_MANIFEST_FILE、二进制旁的.runfiles树/清单多候选,见 runtime.go)。 - 更新检查关闭:每次 spawn CLI 都会附加
TREEHOUSE_NO_UPDATE_CHECK=1(runtime.go)。 - Git 子进程统一加
-c core.fsmonitor=false:避免启动或使用工作区的文件系统监视器守护进程(git.go),测试对此有专门断言(treehouse_test.go)。 - 可测试性:
Runtime接口(runtime.go)抽象了 cwd、环境变量、时钟、UUID、文件读写与 spawn,测试用FakeRuntime完全接管副作用;execute(argv, rt)是纯函数式入口,treehouse_test.go 与 fakeruntime_test.go 覆盖了从成功路径到畸形 JSON、回滚失败、审批面拒绝(write destroy、--force、缺--workspace)等全部关键分支。 - 统一 JSON 信封:成功为
{"ok":true,"data":...},失败为{"ok":false,"error":...,"details":...};renderJSON关闭 HTML 转义并缩进输出(main.go)。
结语:把"租约"当作一等公民
Treehouse skill 与其 Go 包装器回答了一个实际问题:在 IntelliJ Platform 这种巨型 monorepo 中,如何让 AI Agent 获得隔离工作区,同时不破坏其他人的会话、不引入失控的隔离机制、不产生无法归还的租约。它给出的答案是三层约束——skill 文档约束 Agent 行为(命令面 + 操作纪律)、包装器约束底层 CLI(只暴露status/get/return的租约生命周期,永不--force)、实时池约束每次操作的结果判定(归还成功与否看实时租约状态而不是退出码)。阅读本仓库时,建议按 .claude/skills/treehouse/SKILL.md → .ai/workspace-isolation.md → build/treehouse 源码 → treehouse_test.go 测试的顺序深入,即可完整掌握这套工作区隔离体系。
【免费下载链接】intellij-communityIntelliJ IDEA & IntelliJ Platform项目地址: https://gitcode.com/GitHub_Trending/in/intellij-community
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考