首页
/ OpenHuman 的 `/ship-and-babysit` 命令:一条从 Commit 到 PR 全绿自动盯梢的交付流水线

OpenHuman 的 `/ship-and-babysit` 命令:一条从 Commit 到 PR 全绿自动盯梢的交付流水线

2026-09-05 10:33:23作者:平淮齐Percy

/ship-and-babysit 是 OpenHuman 仓库中一条 Claude Code 斜杠命令(定义于 .claude/commands/ship-and-babysit.md),它把「提交代码 → 推送到 fork → 向上游开 PR → 每 5 分钟轮询 CI 与 CodeRabbit 评论并修复问题,直到全部变绿」这一完整交付闭环固化为一个可重复执行的流程。读完后,你将掌握 OpenHuman 的 fork-only 协作模型、PR 模板与检查清单的机器校验规则、diff-cover 80% 变更行覆盖率门槛的底层实现,以及如何用 gh CLI 与 GitHub GraphQL API 完成 CI 状态抓取、CodeRabbit 评论线程的回复与解决等实战操作。

整体流程:四个阶段与 fork-only 原则

该命令要求按顺序执行四个阶段:Phase 1 Commit → Phase 2 Push → Phase 3 Open PR → Phase 4 Babysit loop(约每 5 分钟一轮),每个阶段切换时只向用户输出一句简短说明,保持对话简洁。

仓库事实(来自 CLAUDE.md 第 1269-1280 行的贡献章节)构成了整个流程的安全边界:

  • Upstream 是 tinyhumansai/openhuman(不是 fork),PR 目标是 main 分支。
  • 推送一律发到 origin——即用户自己的 fork;upstream 视为 fetch-only,绝不向它推送分支。CLAUDE.md 中给出的推荐 remote 配置为:
origin    git@github.com:<your-username>/openhuman.git  # push here
upstream  git@github.com:tinyhumansai/openhuman.git      # fetch-only
  • PR 用 --head <fork-owner>:<branch> 打开,目标仓库为 tinyhumansai/openhuman:main
  • 功能开发必须先有对应的 E2E 覆盖,这是「ship」的前置条件。

流程是 fork-only 的:origin 必须是用户的 fork。如果解析出 origin 指向 tinyhumansai(上游组织本身),必须停下来让用户添加 fork remote,绝不能把分支推到上游仓库。

流程开始时先一次性解析 fork owner 并在整个流程中复用:

FORK_OWNER=$(git remote get-url origin | sed -E 's#.*://[^/]+(\.git)?$#\1#')

这条 sed[:/] 同时兼容 SSH 形式(git@github.com:owner/repo.git)和 HTTPS 形式(https://github.com/owner/repo)的 remote URL,从最后一级路径中提取用户名。

Phase 1 — Commit:理解变更、遵循提交规范

提交阶段的第一步是并行执行 git statusgit diff(含已暂存与未暂存)以及最近的 git log,目的是同时了解待提交内容和仓库既有的 commit message 风格。

命令定义了若干明确的分支逻辑与禁令:

  1. 快速通道:如果没有可提交的变更、分支已经推送、且 PR 已经存在,则直接跳到 Phase 4(盯梢循环),避免无意义的重复提交。
  2. 暂存与提交:存在未提交变更时,先暂存相关文件(明确排除 secrets、大二进制文件、.env),然后用 conventional prefix(feat:fix:refactor:chore:docs:test:)创建提交,提交信息使用 HEREDOC 写入。
  3. Hook 纪律:绝不允许对自己造成的 hook 失败使用 --no-verify 绕过。如果 hook 因为自己的改动而失败,正确做法是修复根本问题再创建新提交——不要 amend 已推送的提交。

Feature E2E 规则:ship 之前的覆盖底线

文档单独设立了一节「Feature E2E rule」,规定了不同类别功能变更所要求的 E2E 覆盖位置,这些路径在当前仓库中均可逐一验证:

  • 核心、领域、持久化、CLI、JSON-RPC 功能变更:需要 Rust E2E 覆盖,落在 tests/*_e2e.rs;新的或变更的 RPC 面通常归属 tests/json_rpc_e2e.rs
  • 前端用户流程:需要 Playwright E2E 覆盖,落在 app/test/e2e/specs/*.spec.ts。仓库当前有 100+ 个 spec 文件,如 auth-access-control.spec.tschat-harness-send-stream.spec.ts 等(见 app/test/e2e/specs/)。
  • Mock 到底:E2E 中必须用 scripts/mock-api-server.mjsscripts/mock-api/*app/test/e2e/mock-server.ts 把后端调用全部 mock 掉,禁止在 E2E 中访问真实后端服务或第三方 API。
  • 单元测试不替代 E2E:单元测试对窄逻辑依然重要,但不能覆盖「新构建的功能」所需的 E2E 证明。

推荐的聚焦化运行命令(均可在当前仓库的 package.json / app/package.json 中找到对应 script 定义):

pnpm test:rust:e2e -- --suite <suite>          # 单个 Rust E2E suite
pnpm --filter openhuman-app test:e2e:web:build  # 构建 Web E2E 目标
bash app/scripts/e2e-web-session.sh test/e2e/specs/<spec>.spec.ts  # 跑单个 Playwright spec

其中 test:rust:e2e 实际映射到 scripts/test-rust-e2e.sh,该脚本头部注释明确说明 --suite <name> 可以多次传入以过滤特定 suite——「the cargo-test counterpart to the Tauri E2E specs」。

Phase 2 — Push:分支命名约定与 pre-push hook 策略

推送阶段有三道关卡:

  1. 分支名检查:用 git rev-parse --abbrev-ref HEAD 确认当前分支符合 feat/|fix/|refactor/|chore/|docs/|test/ 前缀约定,绝不直接推 main。若分支名不合规,停下来让用户选择改名或确认偏离——不要自动重命名已推送的分支
  2. 推送到 origin:缺少 upstream tracking 时加 -u。绝不推 upstream,绝不对 main force-push。
  3. pre-push hook 的差异化处置(与 CLAUDE.md 中的 on-push-blockers 策略一致):
    • 若 hook 失败源于与本次改动无关的既有问题(如 main 上你未触碰的代码已经坏了),用 --no-verify 推送,并在 PR 正文中明确说明;
    • 若 hook 失败源于自己的改动,修复后重新推送。
    • 关键措辞是「Don't ask — just do the right thing and tell the user what you did」:授权 Agent 自行做正确的事,但必须告知用户。

Phase 3 — Open PR:模板、检查清单与幂等性

开 PR 前先验证 upstream remote 指向 tinyhumansai/openhuman(缺失时先询问用户再添加)。然后是幂等性检查——确认该分支是否已有 PR:

gh pr list --repo tinyhumansai/openhuman --head <fork-owner>:<branch> --state open --json number,url
  • 若已存在 PR:记录 numberurl,打印 URL,跳过后续步骤直接带 PR# 进入 Phase 4。
  • 若不存在:用 git log main..HEAD 检视提交、用 git diff main...HEAD 检视差异,据此起草标题(<70 字符)与正文。正文必须严格遵循 .github/PULL_REQUEST_TEMPLATE.md

模板的实际结构为:## Summary(3-6 条,聚焦用户可见或影响架构的变更)、## Problem## Solution## Submission Checklist## Impact## Related(含 Closes #NNN),以及针对 AI 生成 PR 的 ## AI Authored PR Metadata 区(Linear Issue、Commit & Branch、Validation Run、Parity Contract、Duplicate/Superseded PR Handling 等字段)。

检查清单有一条机器强制的硬规则:每一项必须勾选;不适用的项写 - [x] N/A: <reason>——未勾选的 N/A 项同样会失败。这条规则的落地在 scripts/check-pr-checklist.mjs 及其解析器 scripts/lib/checklist-parser.mjs

  • 解析器用正则 ^- \[( |x|X)\] (.*)$ 逐行识别清单项(跳过代码围栏内的行),用 NA_REGEX 识别 N/A 前缀并提取理由;
  • 只要 totalUnchecked > 0,脚本以退出码 1 结束,并逐条打印未勾选项,附注「N/A items must still be checked with a reason」;
  • 该脚本由 CI 工作流 .github/workflows/pr-quality.yml 调用(node scripts/check-pr-checklist.mjs),支持从文件、stdin(-)或 PR_BODY 环境变量读取 PR 正文,因此本地也能先跑一遍自检。

PR 正文若绕过过一次 pre-push hook,也必须在这里写入说明。创建命令的完整形态:

gh pr create --repo tinyhumansai/openhuman --base main --head <fork-owner>:<branch> \
  --title "..." --body "$(cat <<'EOF'
...template-filled body...
EOF
)"

最后一步:按仓库惯例添加 labels,记录 PR 编号与 URL——它们在 Phase 4 中会被反复使用。

Phase 4 — Babysit loop:270 秒节拍、12 次硬上限

这是整个命令最有工程含量的部分。循环通过 ScheduleWakeup 工具以 270 秒(4.5 分钟)为节拍自我唤醒——这个数值被刻意选在「prompt-cache 窗口」之内,且每次唤醒时把同一个 /ship-and-babysit 调用作为 prompt 重新传入,实现状态延续。

硬上限:12 个 tick(约 60 分钟)。 超限后停止循环并询问用户,汇报中必须包含 PR URL、当前 CI 快照与仍未解决的 CodeRabbit 线程。为防止计数器漂移,命令要求维护一个显式 tickCount(每次进入循环 +1,无论本 tick 是否产生提交),并把它写进 ScheduleWakeupreason 字段,例如 "tick 5/12: waiting on CI for PR #1115"——即使某个 tick 什么都没做,计数也可见、可审计。

每个 tick 的第 1 步:抓取 CI 状态

gh pr checks <PR#> --repo tinyhumansai/openhuman --json name,state,link,description

这里藏着两个容易被忽略的实战细节,文档都给出了结论:

  • link 字段是 Actions URL 而非 run id(形如 …/actions/runs/<id>/job/<jobId>)。提取 run id 要用对尾部斜杠稳健的正则:

    sed -nE 's#.*/actions/runs/([0-9]+)/.*#\1#p'
    

    而按位置取 awk -F/ 在 URL 带尾部斜杠时是脆弱的。或者干脆跳过 URL 解析:

    gh run list --repo tinyhumansai/openhuman --branch <branch> --json databaseId --limit 1 --jq '.[0].databaseId'
    
  • 区分 Actions 支撑的检查与非 Actions 检查:只有当 link 匹配 /actions/runs/<id>/ 时才用 gh run view <id> --log-failed --repo tinyhumansai/openhuman 拉失败日志;像 CodeRabbit 这类通过 Checks API 直接上报、没有对应 Actions run 的虚拟检查,必须跳过 gh run view,转而基于 name/state/description 字段加上 review 评论来分析。

修复纪律:定位到根本问题后修代码、提交(conventional prefix)、推 origin——绝不通过跳过 hook 或禁用失败测试来把 CI 变绿。

推送修复前的本地复现命令按失败域划分:

失败域 本地复现命令
前端 pnpm typecheckpnpm lintpnpm format:checkpnpm test
Rust cargo check --manifest-path Cargo.tomlcargo check --manifest-path app/src-tauri/Cargo.tomlpnpm test:rust
功能 E2E pnpm test:rust:e2e -- --suite <suite>(核心/RPC 行为);前端流程则 pnpm --filter openhuman-app test:e2e:web:build + bash app/scripts/e2e-web-session.sh test/e2e/specs/<spec>.spec.ts

覆盖率门槛:变更行覆盖率 ≥ 80%。文档中引用的 coverage.yml 在当前仓库里对应的实际执行位置是 .github/workflows/ci-lite.yml(.github/PULL_REQUEST_TEMPLATE.md 与 Issue 模板也都指向 ci-lite.yml)。从 ci-lite.yml 第 1328-1363 行可以看到门槛的真实实现:

  1. 下载所有 lcov-* artifact(Vitest 与 cargo-llvm-cov 各自产出,合并给 diff-cover);
  2. 运行 diff-cover "${LCOV_FILES[@]}" --compare-branch=<base> --fail-under=80 --html-report ... --markdown-report ... --format json:diff-coverage.json
  3. 一个关键的防御性注释:diff-cover 在「diff 中没有任何带覆盖信息的行」时会以退出码 0 通过——注释提到 PR #5593 曾因此带着 1,643 行未编译代码溜过门槛。所以工作流会解析 JSON 报告中的 total_num_lines:若为 0 则发 ::warning:: 提醒「该代码没有被任何 coverage lane 编译过」,而真正判定「文件从未被编译」的硬失败由 scripts/ci/assert-coverage-presence.sh(配合 scripts/ci/coverage-presence-allowlist.txt 白名单)在核心 coverage lane 内完成。这也解释了命令中的一条指引:覆盖率失败时,要为变更行补测试,而不只是补 happy path

每个 tick 的第 2 步:处理 CodeRabbit 评论

先用 REST 分页拉取两类评论:

gh api repos/tinyhumansai/openhuman/pulls/<PR#>/comments --paginate   # review 评论
gh api repos/tinyhumansai/openhuman/issues/<PR#>/comments --paginate  # issue 级评论

过滤 coderabbitai / coderabbitai[bot] 作者。然后对每条未解决的 CodeRabbit 建议做判定:

  • 建议正确且在本 PR 范围内:读它引用的文件/行,直接修复,commit 后推 origin

  • 建议错误或超出范围:先在既有线程内回复再解决。文档特别澄清了一个 API 语义陷阱:

    gh api repos/tinyhumansai/openhuman/pulls/comments/<comment_id>/replies \
      -X POST \
      -f body='**Dismissed:** <reason>'
    

    其中 <comment_id> 是顶层 review comment 的 id;若改用 POST /pulls/<PR#>/reviews 会创建一条全新的 review 线程,而不是在当前线程下回复,这会污染 PR 的评论结构。

解决线程要走 GraphQL API:

gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -f id=<threadId>

列出 thread ID 必须做分页——这是文档中另一处易错点:reviewThreads(first:100) 每页最多 100 条,要循环 pageInfo.hasNextPage / endCursor,把 endCursor 回填为下一页的 $cursor 直到耗尽,否则第 1 页之后的线程会静默逃过退出条件:

gh api graphql -f query='query($owner:String!,$repo:String!,$num:Int!,$cursor:String){repository(owner:$owner,name:$repo){pullRequest(number:$num){reviewThreads(first:100, after:$cursor){pageInfo{hasNextPage endCursor} nodes{id isResolved comments(first:1){nodes{author{login} body}}}}}}}' \
  -F owner=tinyhumansai -F repo=openhuman -F num=<PR#> -F cursor=

每个 tick 的第 3 步:退出条件(三条同时成立)

  1. 所有必需检查均为 SUCCESSPENDING 会让循环继续,没有例外——CI 还在跑时不许宣称「green」;
  2. 没有未解决的 CodeRabbit review 线程;
  3. 自上上个 tick 以来没有新的、要求修改的 CodeRabbit issue 评论。判定方式利用了 GitHub issue-comment id 的单调性:记住上一个 tick 见过的最大 CodeRabbit issue-comment id,本 tick 只把严格大于该标记的 id 视为新评论。

三条同时成立时,不要再调用 ScheduleWakeup,直接返回一行最终摘要(含 PR URL 与当前状态)。否则以 delaySeconds: 270prompt: "/ship-and-babysit"、具体的 reason(如 "waiting on CI for PR #123""applied 2 CodeRabbit fixes, re-checking")调用 ScheduleWakeup 进入下一拍。

Guardrails:五条不可逾越的红线

命令末尾的 Guardrails 是整个流程的安全契约,值得逐条对照:

  • 永不推 upstreamtinyhumansai/openhuman——只推 origin(用户 fork),upstream 视为 fetch-only;
  • 永不对 main force-push,永不 amend 已推送的提交
  • 永不用 --no-verify 绕过自己改动导致的 hook 失败。唯一被授权的绕过场景是「pre-push hook 因既有无关损坏而失败」,且必须在 PR 正文中说明;
  • 永不在真正处理问题(或回复了有理有据的 Dismissed)之前解决 CodeRabbit 线程
  • 遇到需要人类输入的阻塞——认证失败、含糊的 CodeRabbit 建议、互相矛盾的反馈、merge 冲突、vendored tauri-cli 缺失——停下来问用户,而不是猜;
  • 不要合并 PR。流程的终点是「green and clean」,merge 决策留给人。

小结:这条命令把哪些工程判断固化了下来

/ship-and-babysit 的价值不在命令本身,而在于它把大量「只有踩过坑才知道」的判断写死成了规则:fork-only 的 remote 纪律、gh pr checks 返回 Actions URL 而非 run id 的解析陷阱、review 回复与新建 review 线程的 API 语义差异、GraphQL 分页的静默漏读、diff-cover 零行通过假象背后的 coverage-presence 双保险、以及 CodeRabbit 评论 id 单调性用于增量检测。配合 .github/PULL_REQUEST_TEMPLATE.md 的机器校验(scripts/check-pr-checklist.mjs)、.github/workflows/ci-lite.yml 的 80% 变更行覆盖率门槛与 app/test/e2e/specs/ 的 Playwright 覆盖要求,它构成了一套「提交即可信、PR 即自解释、绿了即干净」的自动化交付管线,也是把 Agent 当作交付值班员时最需要的约束集。

登录后查看全文
热门项目推荐
相关项目推荐