feat(pi-worktree): 新增斜杠命令 git worktree 管理扩展

提供 /wt(别名 /worktree)一键拆分/合并:
- /wt new 建 worktree 后经 SessionManager.forkFrom + switchSession
  迁移会话 cwd,历史上下文保留;合并/切换同理
- /wt merge 先确认再 rebase 到来源分支、默认 --ff-only(可选
  --squash),来源改动用 stash 兜底,冲突可中止或保留后用
  /wt merge --continue / --abort 处理,永不 push
- 来源 checkout 的 .pi/worktree.json 声明环境复刻(copy/link/env/
  postCreate),确认框先展示计划再执行;postCreate 仅在项目已信任
  时以 argv 形式 execFile,失败可 /wt reopen 增量重跑
- 登记文件 <agentDir>/pi-worktree/state.json 原子写入并加锁
  串行化;会话切换后向模型注入 cwd 变更提示;未被 .gitignore
  覆盖的软链/环境文件写入 .git/info/exclude 防止误提交
- 仅注册用户显式触发的斜杠命令,不暴露模型可调用工具

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
云服务部-叶林立
2026-10-19 17:45:41 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent f237025cf9
commit 97c2e75dfe
22 changed files with 3195 additions and 1 deletions
+80
View File
@@ -0,0 +1,80 @@
# pi-worktree
本仓库自维护的 Pi 扩展:只用斜杠命令管理 git worktree,提供「一键拆分」(`/wt new`)与「一键合并」(`/wt merge`)。不注册任何模型可调用的工具。
所有 git 操作都通过 `node:child_process` 的 `execFile` 以 argv 形式调用(不经过 shell),普通命令 30 秒超时,`worktree add` / `remove` 为 120 秒。因为这些命令由用户显式触发,它们不经过 Bash 工具的权限策略。
## 命令
`/wt` 与别名 `/worktree` 等价;不带参数时弹出子命令菜单。
| 命令 | 说明 |
| --- | --- |
| `/wt new <slug> [--from <ref>]` | 在 `<root>/<仓库名>/<slug>` 创建 worktree,签出新分支 `<branchPrefix><slug>`(默认 `wt/<slug>`),确认后把当前会话切换进 worktree |
| `/wt merge [--squash] [--keep] [--continue] [--abort]` | 在当前 worktree 内执行:确认 → rebase 到来源分支 → 快进合并(或 squash)回来源分支 → 切回来源目录并清理 worktree 与分支。`--keep` 保留 worktree、分支和登记;rebase 冲突保留时可用 `--continue`/`--abort` 续跑或放弃 |
| `/wt switch [<slug>\|main]` | 切换到某个登记的 worktree;`main` 切回该 worktree 的来源 checkout。无参数时弹列表 |
| `/wt list` | 列出登记的 worktree:分支、路径、`(缺失)`、`(有未提交改动)`、`+N` 领先数,以及非本扩展管理的其它 git worktree |
| `/wt remove <slug> [--force]` | 删除指定 worktree 与分支;有未提交改动或未合并提交时要求 `--force` 并二次确认。不能删除当前所在的 worktree |
| `/wt prune` | 执行 `git worktree prune` 并清除目录已不存在的登记项 |
| `/wt reopen [<slug>]` | 重新读取来源目录的 `.pi/worktree.json` 并补跑环境初始化(已存在的文件/软链自动跳过);默认 slug 为当前所在 worktree |
## 流程与安全规则
- **拆分**:确认对话框显示新分支、基准 ref 和短 SHA、目标路径;创建并登记后才切换会话。会话切换失败不会回滚已创建的 worktree,可用 `/wt switch <slug>` 重试。
- **合并**:必须先校验来源 checkout 仍在登记的分支上;worktree 未提交改动先经确认后 `add -A` + commit;随后展示 diffstat 并要求确认——rebase 改写分支的动作只发生在确认之后。确认后若来源目录有未提交改动先 `stash push`,再 rebase 到来源分支并合并,无论成败都在 finally 中 `stash pop`(pop 失败时提示 stash 名称)。rebase 冲突时弹出选择:「中止 rebase,保持原状」或「保留冲突,稍后手动解决」;保留后在 worktree 中解决冲突并 `git add`,再运行 `/wt merge --continue` 续走合并流程,或用 `/wt merge --abort` 放弃。默认只允许快进合并,`--squash` 压缩为单个提交;**永远不会 push**。
- **删除**:拒绝删除当前所在的 worktree;有未提交改动或领先提交时必须 `--force` 并明确列出将丢弃的内容。
- **状态栏**:进入登记的 worktree 后 footer 显示 `🌳 <slug>`,离开后清除。
## 会话切换机制
Pi 不支持修改进程 cwd。`/wt new` / `merge` / `switch` 通过 `SessionManager.forkFrom`(或为空会话写入最小 session 文件)生成 header.cwd 指向目标目录的新会话文件,再调用 `ctx.switchSession` 让 Pi 以新 cwd 重建运行时;切换后的收尾工作在 `withSession` 回调里用新 `ctx` 完成。
切换成功后,扩展会先往新会话追加一条 `[pi-worktree] 工作目录已从 … 切换到 …` 的模型可见提示(`customType: pi-worktree`、`display: true`,空闲时直接写入 session 文件,不触发新轮次),告知此前旧 cwd 下的路径可能已不存在。
## 状态登记与锁
登记文件为 `<agentDir>/pi-worktree/state.json`(目录 `700`、文件 `600`、原子写入)。所有写操作经 `updateState` 在 `<path>.lock` 锁文件内「读取-修改-保存」,防止多个 Pi 进程互相覆盖登记;锁文件陈旧 30 秒会被接管,正常操作下最长等待 5 秒。读取(list、补全、findByCwd)不加锁。
## 环境复刻:`.pi/worktree.json`
在**来源 checkout**(读取来源是 `entry.sourcePath`,不是新 worktree)提交 `.pi/worktree.json` 后,`/wt new` 创建 worktree 时会按顺序执行 复制 → 软链 → 环境文件 → postCreate 四个阶段,确认对话框会预先列出完整计划:
```jsonc
{
"version": 1, // 必填
"copy": [".env.local", ".env.*.local", "config/local.*"],
"link": ["node_modules", ".gradle"],
"env": { "PORT": "{{port}}", "DB_NAME": "app_{{slug}}" },
"envFile": ".env.worktree", // 默认 .env.worktree
"portBase": 3000, // 默认 3000
"postCreate": [
{ "command": "npm", "args": ["ci"], "timeoutMs": 600000 }
]
}
```
规则:
- **复制**:只复制未被 git 跟踪的文件(已跟踪文件应通过提交共享);`*` / `?` 通配符只允许出现在路径最后一段,不支持 `**`。目录暂不复制。
- **软链**:目标必须被 gitignore 忽略,否则拒绝软链以避免改动写回来源目录。
- **环境文件**:`env` 渲染为 `KEY=value` 逐行写入 `envFile`(权限 `600`);模板变量:`{{slug}}`、`{{pathSlug}}`、`{{branch}}`、`{{path}}`、`{{sourcePath}}`、`{{sourceBranch}}`、`{{port}}`,其中 `port = portBase + fnv1a32(slug) % 1000`。
- **info/exclude 兜底**:未被 `.gitignore` 覆盖的软链(如 `node_modules/` 尾斜杠规则不匹配符号链接)和环境文件会写入 `.git/info/exclude`(仓库内所有 worktree 共享同一 common dir),确保 `/wt merge` 的 `commitAll` 不会把它们提交进来源分支。
- **postCreate**:仅当项目受 Pi 信任(`ctx.isProjectTrusted()`)时执行,未受信任的项目只列出步骤并跳过;步骤以 argv 形式调用 `execFile`(不经 shell),并向步骤注入 `PI_WT_PATH` / `PI_WT_SOURCE_PATH` / `PI_WT_BRANCH` / `PI_WT_SOURCE_BRANCH` / `PI_WT_SLUG` / `PI_WT_PORT` 环境变量;任一步失败即停止。
- **永不覆盖**:所有产物(复制、软链、环境文件)在目标已存在时跳过,因此 `/wt reopen` 可以安全地增量补跑;初始化结果记录在登记项的 `setup` 字段,`/wt list` 会标注 `(环境初始化失败:<步骤>)`。
## 状态与配置
- 登记文件:`<agentDir>/pi-worktree/state.json`(`getAgentDir()`,遵循 `PI_CODING_AGENT_DIR`),目录权限 `700`、文件权限 `600`,原子写入;文件损坏或版本未知时命令报错中止,不会被静默覆盖。**不**放在 `<agentDir>/my-pi/` 下,因为该目录每次升级会被整体替换。
- `settings.json` 的 `piWorktree` 键:
- `root`:worktree 根目录,默认 `~/.pi/worktrees`(只展开开头的 `~`,展开后必须是绝对路径)。worktree 路径为 `<root>/<主 worktree 目录名>/<slug>`。
- `branchPrefix`:新分支前缀,默认 `wt/`。
- slug 规则:`A-Za-z0-9._-` 与 `/` 分层,最长 64 字符,不能以 `-` 开头、不能含 `..`、不能以 `.lock` 结尾,保留名 `main` / `HEAD` 不可用。
## 测试
```bash
npm test # node --test test/*.test.ts
npm run check
```
测试在临时目录里创建真实 git 仓库;状态与会话测试使用隔离的 `PI_CODING_AGENT_DIR`。