在 IntelliJ Platform 仓库中使用 Treehouse 工作区租约生命周期:从 Skill 指南到 Go 包装器实现解析
2026/9/18 3:02:44 网站建设 项目流程

在 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 enterinitupdateprunedestroy--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,也不暴露enterinitupdateprunedestroy--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严格校验每条记录的namepathstatus字符串字段以及lease_idlease_holderprocesses的类型(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)。这个--forceGit的 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_versionpathlease_idlease_holderacquired_atsource_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):

  1. 拒绝从租约工作区内部运行(防止包装器自身变成"活进程");
  2. 读取并解析工作区内收据(schema v2),且收据中的path必须与请求路径一致;
  3. 校验路径是 Git 工作区根;
  4. readStatus拿实时租约,收据的lease_id+lease_holder必须与实时状态完全匹配(双身份守卫);
  5. 有活进程则拒绝;
  6. 工作区脏(git status --porcelain=v1 --untracked-files=all有输出)且未带--confirm-preserved时拒绝;
  7. 调用treehouse return <path> --if-lease-id <id> --if-lease-holder <holder>(见 receipt.go 的returnCommand);
  8. verifyReturned再次查询实时池确认租约已消失,以实时租约状态为准,而非退出码(returncmd.go 及 status.go 的confirmLeaseGone);
  9. 只有确认成功后,才删除收据。

脏工作区的确认标志

./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 步操作纪律:

  1. acquire 前先检查内置的request_permissions工具是否可用;不可用则不要获取租约,并报告本会话无法使用 Treehouse——不得要求用户修改权限设置、以--add-dir重启或授予 Treehouse 池访问权;
  2. 从本 skill 目录运行,并请求两个审批前缀:../../../community/tools/treehouse.cmd read../../../community/tools/treehouse.cmd write(Community checkout 中去掉community/)。write 审批只让包装器触达 Treehouse 池,并不授权acquire 或 return 本身;
  3. 工具的 cwd 变更不会把租约工作区加入会话可写根。保持从原始 checkout 运行,用request_permissions恰好返回的那个 path申请会话级写访问(在任何编辑之前)。不要申请 Treehouse 池、源码 checkout、共享 Git 目录或全量访问,也不要改用逐命令提权;授权后,后续工具一律以该工作区路径为工作目录;
  4. 授权被拒时,不得进入、编辑或在租约工作区中运行命令,立即从原始 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_DIRRUNFILES_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询