repo-tidy

Prepare safe task branches/worktrees, preserve active or unverified work, and keep session working directories in sync. Use for new repository-changing tasks or requested cleanup; not for read-only au

SKILL.md

GitHub

repo-tidy

把本地仓库恢复到「默认分支与远端一致、无可安全回收的遗留分支/worktree」的基线状态。默认分支优先读取 origin/HEAD,缺少该引用时才兼容 master/main。归位发生在下一个任务开始时(push 完 MR 未合,任务结束时无收尾时机)。

组件(脚本就地运行于 skill 目录,不复制副本):

  • scripts/repo_tidy.py —— 核心:tidy / --all / --new
  • scripts/git-repo-status.sh —— SessionStart hook:注入 [repo-status](分支/ahead-behind/脏净)+ [tasks] 菜单(各 worktree 的分支与状态:已合并可回收 / 已推送 MR 待合 / 未推送 / 工作区脏),供判断续任务还是开新任务
  • scripts/session-cwd.sh —— Claude Code 按终端键记录 cwd;Codex 用 --agent codex 按真实 thread ID 隔离。静默输出(挂在 SessionStart/UserPromptSubmit 上,stdout 会被注入上下文)
  • scripts/codex-session.py —— 保存 Codex 会话目录及终端绑定,按客户端 PID、启动时间校验归属;共享后端继承的终端环境变量不能作为绑定依据
  • scripts/worktree-fetch.sh —— PreToolUse(EnterWorktree) 先 fetch --prune,保证新任务分支从最新的 origin/<默认分支> 切出
  • scripts/install.sh —— 一键注册上面几个 hook 到 ~/.claude/settings.json;幂等、改前备份、只增不删
  • scripts/editor-here.sh —— 绑到编辑器快捷键:按前台标签页 id 读位置文件,打开 AI 当前所在目录;读不到再回退到终端路径
  • scripts/task-here.sh —— 声明「本 session 当前在做哪个任务目录」,给运行中不能切 cwd 的 agent(Codex)用;Claude Code 不需要
  • tests/ —— test_repo_tidy.sh、test_session_cwd.sh、test_install.sh(另有 scripts/test_git_repo_status.sh),改脚本后必须跑

安装

前置:python3、git、可写的 ~/.claude/。缺任一项先停下说明,不带病安装。

私人配置(settings.json / CLAUDE.md)不进版本库,所以用脚本改——幂等、改前备份、升级自己的旧命令,不碰用户已有的其它 hook:

bash "$SKILL_DIR/scripts/install.sh"             # 安装(可反复跑)
bash "$SKILL_DIR/scripts/install.sh" status      # 体检:依赖 / 核心层 / 增强层 / hook 注册
bash "$SKILL_DIR/scripts/install.sh" --uninstall # 只移除本 skill 注册的 hook

Claude Code 与 Codex 都装,检测到哪个装哪个(CLAUDE_HOME / CODEX_HOME 可覆盖路径,测试用)。Codex cwd hook 使用 --agent codex,读取 stdin 的 session_id 与 cwd;升级时替换旧 hook 命令,避免重复注册。已安装的无参数命令会从祖先 agent 进程识别 Codex 并走同一隔离流程,更新源脚本即可生效,无需为了本次修复变更已有 hook 信任。

事件 matcher 脚本 Claude Code Codex
SessionStart * git-repo-status.sh、session-cwd.sh ✓ ✓
UserPromptSubmit * session-cwd.sh ✓ ✓
PostToolUse EnterWorktree|ExitWorktree session-cwd.sh ✓ —
PreToolUse EnterWorktree worktree-fetch.sh ✓ —

后两条只给 Claude Code:Codex 没有 EnterWorktree 工具,它用原生的 codex --worktree(启动时就进 worktree)和 -C/--cd。

hook 热生效,装完不用重启 session。Codex 首次运行会要求确认信任新 hook。

Codex:对话中决定任务目录,不必启动前就想好

Codex 运行中不能切工作目录,但能用绝对路径在别的目录里干活(已实测)。所以流程和 Claude Code 一样是「先聊、再决定」,只是多一步声明:

  1. 在项目主路径启动 codex,照常对话
  2. 判断是新任务且需要独立目录 → 先问用户,同意后:
    python3 "$SKILL_DIR/scripts/repo_tidy.py" . --new <task>
    
    主检出空闲就原地切分支(此时 cwd 就是任务目录,第 3 步可跳过);被占用则建 worktree 并输出路径
  3. 若拿到的是 worktree 路径,声明它,好让用户的编辑器快捷键跟过去:
    bash "$SKILL_DIR/scripts/task-here.sh" <worktree 路径>
    
    之后用绝对路径在那个 worktree 里读写、跑 git -C <路径> ...
  4. 任务结束 task-here.sh --clear

不要让用户用 codex --worktree 或 -C 启动——那等于逼人在开口之前就想清楚要做什么,和「先聊再决定」是相反的。这两个参数只在用户主动要求时才提。

声明存在 <键>.task,优先级高于 hook 写的 <键>:后者每轮对话都会被刷新成 cwd,不分开存会被冲掉。Claude Code 不需要声明——EnterWorktree 真的改了 cwd,hook 自动就写对了。

Codex 声明改为 codex-<thread ID>.task,cwd 存在 codex-<thread ID>。共享 app-server 的 TERM_SESSION_ID / tty 属于启动后端的终端,多个会话会继承同一份,不得用它们写终端位置文件或猜测绑定。

独立后端(codex --no-daemon)可从祖先 TUI 客户端自动建立绑定。共享后端使用明确绑定:

  1. 核验终端 UUID、该终端的 Codex TUI 客户端及当前 thread ID;不要按仓库 cwd 或前台窗口猜测。同一目录可以有多个会话。
  2. 在当前 Codex 会话调用:
    bash "$SKILL_DIR/scripts/task-here.sh" --bind <已核验的终端 UUID>
    
    脚本从 CODEX_THREAD_ID / CODEX_SESSION_ID 获取当前 thread,核验该 UUID 有唯一的 TUI 客户端后保存绑定。缺 ID、无客户端或归属不唯一时拒绝。
  3. 任务目录继续用 task-here.sh <目录> 声明;每轮 cwd 刷新不会冲掉它。

快捷键每次检查客户端 PID 和启动时间。客户端重启、绑定损坏、尚未绑定时回退该终端自己的路径,禁止读取可能属于其它 Codex 会话的旧位置文件。在同一 TUI 切换到另一个 Codex thread 后重新绑定;不要把原绑定当成新 thread 的归属证据。

两层能力,各自独立可用

核心层(仓库归位、任务 worktree、开工前 fetch、[tasks] 菜单)是纯 git 操作,与平台和终端无关,任何环境都能用。

增强层(编辑器快捷键打开 AI 当前所在目录)需要「能报出当前是哪个终端标签页」,只有这层有环境要求。没有它不影响前面任何功能。

增强层:绑编辑器快捷键

把 scripts/editor-here.sh 绑到快捷键(iTerm2 用 Preferences → Keys 的 Send Text / Run Coprocess,或 Hammerspoon / Karabiner 调用)。若快捷键调用自己的 launcher,让 launcher 转调这个脚本;不要复制脚本到 ~/bin,否则相邻的 helper 找不到且后续更新不会生效。

不能直接绑 code .:claude 运行期间终端的当前目录冻结在启动目录,子进程改不了父 shell 的 cwd,AI 切进 worktree 后 code . 打开的还是旧目录。

环境 状态
macOS + iTerm2 已实测
macOS + Terminal.app 已写支持(按 tty 匹配),未实测
其它终端(WezTerm / Alacritty / tmux / VS Code 内置) 需设 SESSION_KEY_CMD,未实测
Linux / WSL 核心层可用;增强层需 SESSION_KEY_CMD + EDITOR_HERE_CMD,未实测

标「未实测」的是照着 API 文档写的,作者手上没有那些环境。能跑通或需要改,欢迎开 issue / PR。

session-cwd.sh 按两个键各写一份位置文件,就是为了让不同终端各取所需:

  • <UUID> —— TERM_SESSION_ID 的 UUID 段,iTerm2 与 Terminal.app 都设这个变量
  • tty-<name> —— 控制终端名,POSIX 通用回退

位置文件第 1 行是 cwd,第 2 行 owner <PID> <启动时间> 记写入它的 agent 进程。agent 退出后标签页和文件都还在,tty 也会被新标签页复用,所以 editor-here.sh 只在 owner 仍存活且启动时间一致时采信该文件(连同同键的 .task),否则回退终端当前路径。没有 owner 行的旧格式文件不采信,运行中的 session 下一轮对话就会被 hook 改写成新格式。

其它终端设 SESSION_KEY_CMD 指定一条输出「当前标签页唯一键」的命令,editor-here.sh 会拿它去查同名或 tty- 前缀的位置文件。例如 tmux:

export SESSION_KEY_CMD="tmux display-message -p '#{pane_id}'"

环境变量:EDITOR_HERE_APP(macOS 上要打开的应用,默认 VS Code)、EDITOR_HERE_CMD(直接指定打开命令,如 cursor / zed)、EDITOR_HERE_DRY=1(只打印路径不打开,排错用)。

排错

先跑 install.sh status,它会分层告诉你卡在哪一环:hook 没注册、终端认不出、位置文件不新鲜(hook 没在跑)、editor-here.sh 不可执行。

快捷键打开了错目录 → EDITOR_HERE_DRY=1 bash "$SKILL_DIR/scripts/editor-here.sh" 看它解析到什么,再看 $TMPDIR/editor-here.log 最后一行的来源标签(iterm-codex 走经过校验的 Codex 绑定,iterm 走 Claude/终端位置文件,terminal-path 说明回退了,前面若有 stale(...) 表示该键的位置文件 owner 已退出)。Codex 检查 <终端 UUID>.codex 的绑定及 codex-<thread ID>[.task],不要用继承的终端环境变量声明任务。

卸载

bash "$SKILL_DIR/scripts/install.sh" --uninstall   # 移除 hook(备份已自动留下)
rm -rf ~/.claude/session-cwd                        # 位置文件

再解绑编辑器快捷键、删除 skill 目录即可。settings.json 的备份在 ~/.claude/settings.json.bak-*。

何时用

  • 用户说「归位」「整理仓库」「清理分支/worktree」
  • 需要修改仓库的新任务开工前;只读审计、问答、续任务不归位,[repo-status] 本身不触发动作
  • 显式调用 do-something 时遵循该 skill 的 do/main 续做规则,不额外开普通任务分支
  • 用户明确要求提交后的仓库整理

执行步骤

1. Dry-run 出清单

SKILL_DIR 指本 SKILL.md 所在目录(全局安装、项目级安装、plugin 缓存目录均适用,按实际安装位置解析,不要硬编码)。

# 单仓库(默认当前目录)
python3 "$SKILL_DIR/scripts/repo_tidy.py" <repo-path>

# 全部仓库(扫描 ~/Desktop/Works/code)
python3 "$SKILL_DIR/scripts/repo_tidy.py" --all

2. 核对范围后执行

核对 dry-run 清单与授权范围。已授权的单仓库清理或按项目规则启动新任务,可直接处理已核实安全的对象,不因换轮次重复询问。未推送、未合并、脏工作区、活跃任务或归属不明的对象保留;范围扩大或存在数据丢失风险时说明影响并确认。

upstream 已删除 本身不能证明工作已合并。脚本只自动清理已证实为默认分支祖先的分支;未证实合并的 gone 分支保留并报告;其关联 worktree 同样保留,但不单独列出目录。不阻塞 --new 创建新任务。squash 等不能由祖先关系证实的情况,也不自动删除;需要另行核实内容保留证据和清理授权。

python3 "$SKILL_DIR/scripts/repo_tidy.py" <repo-path> --apply

3. 开新任务(已有范围授权时直接执行)

**Claude Code 里走 EnterWorktree,不要用 --new。**读 SessionStart 注入的 [tasks] 菜单判断:

  • 续某个任务 → EnterWorktree(path=<该 worktree 路径>)
  • 新任务 → EnterWorktree(name=<task>)
  • 两个 session 共用一个 worktree(一个写、一个 review)→ 同样用 path
  • 已有任务范围授权时直接进入工作树,不重复确认;只有工作位置会改变目标、范围或重大后果时才确认。用户明确要求留在主检出时遵循。session 中途换新任务:ExitWorktree(keep) 再 EnterWorktree(name=...),同样复用已有授权

配套 hook 保证两件事:进门前已 fetch(新分支基于最新远端默认分支),进门后位置文件已更新(用户快捷键看到的就是这里)。主检出因此恒定停在默认分支,只作阅读窗口——默认不在主检出上开工,用户明确要求时除外,否则第一个任务就会把它占走,后续任务被迫绕到 worktree,而没人再把它还回来。

没有 EnterWorktree 的环境(如其他 CLI)才用回退命令:

python3 "$SKILL_DIR/scripts/repo_tidy.py" <repo-path> --new <task>
  • 先对该仓库执行一次归位(等价 --apply,安全边界相同)
  • 主检出空闲(在默认分支且无 tracked 改动)→ 原地 switch -c task/<task> origin/<默认分支>,这会占用主检出
  • 主检出被占用 → 创建 sibling worktree <仓库>--<task>(基于最新 origin/<默认分支>)并输出 cd 路径
  • 若占用主检出的分支已经合并进默认分支(任务做完没归位的僵尸),会明确报出来并给归位命令,而不是默默绕开——绕开正是主检出被长期霸占的原因:每个新任务都躲去 worktree,没人回来收拾
  • <task> 含 / 时按原样作分支名,否则加 task/ 前缀

脚本行为(安全边界)

对象 条件 动作
本地分支 已合并进 origin/<默认分支> 删除
本地分支 upstream 已删除 已证实合并则按上一行清理,否则保留并报告
本地分支 有未推提交 / 从未推送 / MR 进行中 保留并报告
本地分支 与 origin/<默认分支> 同点且未推送过(刚切出的空任务分支) 保留(并行 session 可能正要用)
本地分支 空且已落后 origin/<默认分支>(切出后从未动过) 删除(无内容可丢,重切才是正确归位)
当前分支 可清理且工作区无 tracked 改动 切回默认分支再删
默认分支 落后远端 ff-only 前进
默认分支 与远端分叉 不动,报告
worktree 分支可清理且工作区干净 移除
worktree 工作区脏 / detached 保留并报告

脚本自身永远可以 dry-run;一切「✋ 保留」项需要人工决策,不要替用户强清。

不做的事

  • 不删除整目录的克隆分身(如 xxx-mr199/ 这类完整 clone)——发现时报告给用户,删目录是危险操作需单独确认
  • 不 force push、不改写历史、不碰远端分支