Remotion 仓库 Agent 技能实战:用 gh-stack 管理堆叠分支与 Pull Request
【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion
本篇围绕 Remotion 仓库内置的 Agent 技能文档 SKILL.md 展开,系统讲解 GitHub CLI 扩展gh stack(即 gh-stack)用于堆叠分支(stacked branches)与堆叠 PR 的完整工作流:从安装配置、非交互调用的安全约束,到创建堆叠、同步、合并、状态读取与退出码恢复策略。读完后,你既能亲手操作gh stack完成多段式工作的分层提交与评审,也能理解该技能为何以"命令行为契约 + 退出码协议"的方式编写——这是面向 AI Agent 的工具编排文档的一个典型范例。
该文档位于 Remotion 仓库的 Agent 技能目录 .agents/skills/gh-stack/ 下,与 AGENTS.md 所描述的仓库级 Agent 工作约定配套:Remotion 仓库在.agents/skills/下维护了数十个技能(如add-effect、release、pr等),gh-stack专门负责"把多部分工作拆分为可评审的分支栈"这一场景。
什么是 gh stack:堆叠分支与 Stack 的基本概念
gh stack是 GitHub CLI 的一个扩展,用于管理堆叠分支和堆叠的 Pull Request。一个 stack 是一条以 trunk 为根、有序的分支链,每个分支基于它下面的分支各开一个 PR,因此评审者每次只看得到该层自己的 diff。
gh stack打印堆叠时以 trunk 为起点、从左到右排列:
(main) <- auth <- api <- frontend左端是底层,右端是顶层。auth基于main,最先合并;frontend最后合并。导航方向上:up远离 trunk、朝顶部移动;down朝 trunk 移动。依赖关系决定分层——基础性工作放在底部,依赖它的代码放在上面。关于如何决定分几层、每层放什么,见 stack-design.md。
安装与环境准备
官方文档要求三条配置,前两条是功能开关,第三条在多 remote 仓库中是硬性前提:
gh extension install github/gh-stack git config rerere.enabled true # 记住冲突的解决方案(rerere) git config remote.pushDefault origin # 仓库有多个 remote 时必须配置结合 commands.md 的行为说明,这三条命令背后的机制值得展开:
gh stack init会自动启用git rerere(首次运行在 TTY 下会询问确认,提前设好rerere.enabled true可跳过)。由于堆叠分支中"底层的一次变更会被 rebase 穿过其上所有分支",同一冲突会反复出现,rerere 让首次解决后的方案自动重放,这是堆叠工作流里最重要的体验优化;checkout和trunk命令没有--remote参数,完全依赖remote.pushDefault配置;对push、submit、sync、rebase、link而言,未配置remote.pushDefault时必须显式传--remote <name>,否则会出错。
非交互调用协议:Agent 使用 gh stack 的核心安全约束
这是整份技能文档中信息密度最高的部分。gh stack的许多命令以stdout 是否为 TTY作为行为分叉点:被管道化时,多数命令干净地报错或输出静态文本;而在 PTY 下,同样的命令会打开交互式提示或全屏 TUI,永久阻塞。由于不同 Agent 运行时(harness)对 TTY 的检测与模拟各不相同,文档明确要求:不要依赖这种检测,始终显式传旗标。
"永远这样跑"对照表(来自 SKILL.md):
| Always run | Never run bare | 原因 |
|---|---|---|
gh stack view --json | gh stack view | PTY 下会打开 TUI |
gh stack submit --auto | gh stack submit | 会为每个新 PR 提示输入标题 |
gh stack merge <target> --yes --squash | gh pr merge | gh pr merge无法合并堆叠 |
gh stack init <branch>... | gh stack init | 会提示输入分支名 |
gh stack add <branch> | gh stack add | 提示输入名称,且管道化时也会失败 |
gh stack checkout <target> | gh stack checkout | 会打开选择菜单 |
gh stack up/down/top/bottom | gh stack switch | switch只有菜单模式 |
| — | gh stack modify | 仅 TUI,无非交互路径 |
两条补充规则:
view --short在两种模式下都安全,但它是给人看的格式;程序解析一律用--json。checkout <pr>在已有另一个本地堆叠覆盖这些分支时无法强制。先执行gh stack unstack --local(保留 GitHub 上的堆叠),再重试 checkout。
这份"命令契约表"本身就是该技能的核心价值:它把一条外部 CLI 的交互式陷阱转化为 Agent 可直接执行的确定性规则,避免任何自动化流程卡在阻塞提示上。
分支放置原则:先分层,再写代码
文档给出两条分支放置规则,分别对应"新开工"和"改现有堆叠":
开始多部分工作时:在写文件之前就创建堆叠。不要在 trunk 上把所有关注点都实现完再事后拆分。每个层放一个相互依赖的关注点,自底向上。
编辑现有堆叠时:先 checkout 到"拥有该变更"的那一层再编辑。永远不要在当前顶层分支上提交属于更下层的关注点。流程是:
gh stack down # 或:gh stack checkout api git add ... && git commit -m "Add get-user endpoint" gh stack rebase --upstack # 把该层之上的所有分支重放到变更之上 gh stack top # 回到你原来的位置 gh stack push执行前先跑gh stack view --json确认归属;若所有权不明确,用git log --all -- <path>查看该路径的提交历史。
配套的 stack-design.md 给出了更完整的分层设计方法论:
先规划层,再写代码
Stack 是一条依赖链:如果 A 层的代码依赖 B 层,依赖方必须位于同一分支或更低的分支。这个约束"规划时满足"远比"事后重排"便宜——因为 gh stack没有非交互的就地重排,改顺序只能unstack再init。
一个示意的 todo 应用分层(文档特别注明:这是举例,应从实际任务推断主题与层名,不要照抄):
(main) <- todo-app/models <- todo-app/api <- todo-app/frontend <- todo-app/integrationtodo-app/models— 共享类型与 schematodo-app/api— 使用 models 的路由todo-app/frontend— 调用路由的组件todo-app/integration— 覆盖整个功能的测试
要避免的典型失败模式:全写在一个分支上再试图事后拆分。如果任务大到值得用堆叠,就从一开始创建堆叠。
分支命名
推荐"共享主题前缀 + 层关注点"的形式:<topic>/<concern>,例如billing/schema、billing/api、billing/ui。注意:用户或仓库的分支命名规范优先;分支名按字面使用——gh stack add refactor/foo创建的就是字面名为refactor/foo的分支,不会做任何前缀或变形。如果只传-m不传分支名,名字会由提交信息生成(日期 + slug 形式,如03-24-add_api_routes);文档建议优先自己命名。
刻意地暂存变更
直接用git add+git commit,而不是add -Am快捷方式——重点是控制哪些变更落到哪个分支。工作树里有多处修改时,只暂存属于当前层的子集并提交,再创建下一层、在下一层暂存其余部分:
git add internal/models/user.go internal/models/session.go git commit -m "Add user and session models" gh stack add api-routes git add internal/api/routes.go internal/api/handlers.go git commit -m "Add user API routes"每个分支可以有多个提交,关键是:该分支的每个提交服务于同一关注点,属于其他关注点的变更进入其他分支。另注意gh stack add <branch>不带-Am时不触碰工作树,未提交的修改会带到新分支上——想让新层干净起步,先提交或 stash。
何时加一层
当你开始一个依赖于已建内容的新关注点时加层。信号:从后端转向前端、从核心逻辑转向测试或文档;下一步变更面向不同的评审人群;当前分支的 diff 已经大到可以独立评审。经验法则:一句话说不清楚的层,通常是两层。
一个堆叠,一个故事
Stack 应该读起来是一个连贯的递进:评审者自底向上走过 PR,看到功能被逐层构建出来。所有分支服务于同一功能/项目就用一个堆叠;无关的工作(另一个功能、无关的 bug 修复、独立的重构)另开堆叠,用gh stack init新建或gh stack checkout <target>在已有堆叠间切换。顺手的小修可以搭当前堆叠的便车,一旦长成独立项目就值得自己的堆叠。
核心工作流:init → add → submit
最小完整闭环如下(来自 SKILL.md 的 "Core loop"):
gh stack init auth # 创建堆叠并 checkout 其分支 git add ... && git commit -m "Add auth middleware" gh stack add api # 下一层,从当前分支分叉 git add ... && git commit -m "Add API routes" gh stack submit --auto # 推送所有分支并创建 draft PR gh stack view --json # 确认submit加--open会创建可直接评审的 PR 而非 draft;不加则新 PR 是 draft。
结合 commands.md,每个命令的精确行为如下,这些细节在--help里看不到,但对正确编排至关重要:
init创建堆叠并 checkout 参数列表中最后一个分支,因此一条gh stack init auth api frontend就能铺好整条链。分支参数自底向上处理:已存在的分支被"收养"(adopt),不存在则创建——第一个从 trunk 创建,之后每个新分支从紧邻的前一个分支创建。不存在单独的 adopt 模式,"存在与否"决定一切。--base用于选择非默认 trunk。add必须从堆叠的顶部分支(空堆叠时从 trunk)执行,否则以退出码5报can only add branches on top of the stack,先跑gh stack top即可。不带-Am时不碰工作树,未提交变更随行;当当前分支尚无提交时(例如init之后),add -Am会就地提交而不是新建分支——这是刻意设计,因为第一层通常要先有内容才谈得上第二层。-A与-u互斥;不带-m会打开编辑器输入提交信息,所以 Agent 必须总是搭配-m。push以单次 multi-ref 推送推所有活跃(未合并、未入队)分支,逐分支带--force-with-lease。非原子:部分分支可能已更新而另一分支被拒;被拒意味着该分支在远端有移动,修好该分支再重跑即可,重跑是安全的(跳过已落地的部分)。push从不创建或更新 PR,那是submit的职责。submit逐个推送活跃分支,为缺少 PR 的分支创建 PR(base 指向第一个未合并的祖先),再在 GitHub 上链接成 Stack。同样非原子:逐个推送、逐分支--force-with-lease,后面被拒时前面的推送与 PR 更新保留,修好重跑即可。两个边界行为:(1) 当堆叠里所有 PR 都已合并时,堆叠不能再扩展——submit会把剩余未合并分支分叉成一个以 trunk 为根的新堆叠,原合并堆叠不动;(2)--auto的标题生成规则:单提交分支用该提交的 subject 做标题、body 做 PR 描述;多提交分支把分支名人性化(连字符和下划线变空格)。没有自定义标题/描述的旗标,事后用gh pr edit改。submit要求仓库开启了 stacked PRs,否则非交互下退出码9(TTY 下会询问是否降级为普通 PR)。link是"纯 API"路径:在不写任何本地跟踪状态的情况下创建或更新 GitHub 上的堆叠。参数自底向上给出,可以是分支名或 PR 号(数字参数先按 PR 号解析,回落到分支名);若第一个数字参数恰好是已存在的堆叠号,则剩余参数追加到该堆叠顶部(gh stack link 7 feature-c),无需重列现有 PR。分支参数会被自动推送(非 force、原子);缺的 PR 用自动标题和正确的链式 base 创建,base 错误的已有 PR 会被纠正。堆叠成员是只增不减的——link永不把 PR 移出堆叠。
保持同步:sync 的八步流水线
gh stack sync # fetch、与 GitHub 对账、rebase、push、刷新 PR 状态 gh stack sync --prune # 额外删除已合并 PR 的本地分支非交互环境下,没有--prune就绝不清理。若本地与远端堆叠发生分叉(divergence),sync会打印两条链、不做任何修改、以退出码0结束并输出Sync aborted——注意这里退出码 0 不代表同步成功,必须检查该消息或重新gh stack view --json比对。
commands.md 给出了sync的完整步骤(按序):
- Fetch远端;
- 与 GitHub 堆叠对账:在 github.com 上加入堆叠的 PR 会被拉下来并在本地追加;分叉时非交互模式中止;
- 快进 trunk(已是最新则跳过,分叉则告警);
- 必要时级联 rebase:trunk 移动、某个堆叠分支从远端快进、或某分支不再包含其预期父分支时触发;已合并 PR 自动处理;冲突时所有分支恢复到 rebase 前状态,退出码3;
- 推送所有活跃分支(原子);
- 从 GitHub 刷新 PR 状态;
- 同步堆叠对象——把开放 PR 链接进堆叠(只增),仅在存在两个及以上 PR 时;
sync永不打开 PR,那是submit的事; --prune时才清理已合并 PR 的本地分支(非交互环境)。
rebase命令单独使用时从远端拉取并级联 rebase,适用于sync报告冲突后或只需 rebase 堆叠一部分的场景,关键旗标:
--upstack:从当前分支到顶部 rebase——编辑了底层之后的标准操作;--downstack:从 trunk 到当前分支 rebase;--no-trunk:完全跳过 fetch 与 trunk rebase,只对齐堆叠内分支;--continue在暂存好解决方案后继续;--abort恢复所有分支;- 已合并 PR 会被自动识别,并用
--onto重放到正确目标上,因此 squash-merge 过的父分支不会产生伪冲突; - 已有 rebase 进行中时再启动 rebase,退出码7。
合并:范围明确、全有或全无
文档明确:只有当用户明确要求合并时才执行堆叠合并。先检查gh stack view --json,弄清用户要合并整个堆叠还是只合并到某个 PR 为止,然后带显式方法合并:
gh stack merge 42 --yes --squash # PR #42 加上它下面所有未合并的 PR gh stack merge 7 --yes --squash # 堆叠 #7 中所有未合并的 PR语义细节(与 commands.md 一致):
- 传PR 号= 合并该 PR 及其下方所有未合并 PR;传堆叠号= 合并该堆叠内所有未合并 PR;
- All-or-nothing:合并集合中任何一个 PR 不能合并,则一个都不合并,并报告原因;
- 方法来自
--squash、--rebase、--merge或--merge-method <method>;省略时会复用上次的合并方法——但 Agent 必须显式传,不能依赖记忆态; - 合并前只检查基础 PR 状态(open 且非 draft),不支持绕过合并要求;
- base 分支配置了 merge queue 时会覆盖一切:堆叠被加入队列而不是直接合并,由队列选择合并方法,你传的方法旗标会被忽略并给出警告;入队的 PR 一起提交,但可能作为多个分组先后落地,而不是同时落地;
gh pr merge无法合并堆叠,必须用gh stack merge。
状态读取:view --json 的 Schema
gh stack view --json把 JSON 写到stdout;状态消息走stderr——不要解析 stderr,基于退出码做分支判断。
trunk string currentBranch string branches[] name, head, base, isCurrent, isMerged, isQueued, needsRebase branches[].pr number, url, state ("OPEN" | "MERGED" | "QUEUED");PR 不存在时缺省两个字段值得注意:
base是该分支最后一次已知包含的父分支的 SHA 快照,可能比父分支当前 tip 更旧;needsRebase为 true 当且仅当当前父分支 tip 不再是该分支的祖先。
也就是说,程序判断"哪些层需要重放"不需要自己算 git 祖先关系,直接读needsRebase即可。
退出码协议与恢复策略
这是该技能文档为自动化消费专门设计的部分。完整退出码表:
| 退出码 | 含义 | 恢复动作 |
|---|---|---|
| 0 | 成功 | — |
| 1 | 一般错误 | 读 stderr |
| 2 | 不在堆叠中 | gh stack init或gh stack checkout <target> |
| 3 | Rebase 冲突 | 见下文 Exit 3 恢复 |
| 4 | GitHub API 失败 | 检查gh auth status,重试 |
| 5 | 参数无效 | 修正调用;参考<command> --help |
| 6 | 需要消歧 | 分支属于多个堆叠;checkout 一个不共享的分支 |
| 7 | Rebase 已在进行中 | gh stack rebase --continue或--abort |
| 8 | 堆叠文件被锁 | 另一个gh stack进程在写;约 5 秒后重试 |
| 9 | 仓库未启用 Stacked PRs | 告知用户 |
| 10 | Modify 恢复态 | gh stack modify --abort |
Exit 3(rebase 冲突)恢复,区分两个来源:
- 在
gh stack rebase之后:解决文件、git add,然后gh stack rebase --continue;gh stack rebase --abort可整体恢复堆叠(恢复的是所有分支,不只是当前分支); - 在
gh stack sync之后:堆叠此时已经恢复原状。重新跑gh stack rebase复现冲突,再按上述方式解决并 continue。
由于init启用了git rerere,一次解决过的冲突在下一次同样冲突出现时会被自动重放——这在堆叠场景里非常常见,因为底层的一次变更会被 rebase 穿过其上每一层。没有 rerere 时,重复冲突可能需要在每个受影响的层上手动解决。
典型故障场景与恢复手册
troubleshooting.md 覆盖了六类真实场景,摘要如下:
Squash merge 之后
Squash merge 把分支的提交替换成 trunk 上的一个新提交,原提交不再存在于 trunk 历史中,普通 rebase 会试图重放它们。gh stack sync会检测这种情况并用--onto对正确目标 rebase、跳过已合并分支:
gh stack sync gh stack view --json # 已合并分支报告 "isMerged": true, "state": "MERGED"无需手动操作;若重放冲突,sync会恢复所有分支并退出 3,再按 Exit 3 流程处理。需要时加gh stack sync --prune删除已合并 PR 的本地分支。
本地与远端堆叠分叉
非交互下sync会打印两条链、零修改、退出码0并输出Sync aborted。两条恢复路径(都不删除任何 PR 或分支):
保留远端版本:
gh stack unstack --local # 保留 GitHub 上的堆叠 gh stack checkout <stack-number> # 或 PR 号保留本地版本:
gh stack unstack # 移除分组;PR 和分支保留 gh stack submit --auto注意:远端 unstack 会让处于 auto-merge 或 merge queue 中的 PR 保持堆叠状态,必要时先清除该状态再重试。
重组堆叠(无交互重排)
没有非交互的 reorder / rename / remove;add在错误分支执行时会提示gh stack modify,但那是 TUI-only。正确做法是拆了重建:
gh stack unstack # 移除本地跟踪和 GitHub 分组 # 按需重命名/删除分支,改写祖先关系 gh stack init --base main branch-1 branch-2 branch-3 gh stack submit --auto # 在 GitHub 上重新链接init收养已存在分支,所以重建是复用的;已有 PR 存活,祖先关系正确后submit会更新它们的 base 并重新链接。关键提醒:改元数据不等于改 Git 祖先——先重排提交,再重建堆叠。例如把main <- models <- migration <- ui改成main <- migration <- models <- ui:
old_models=$(git rev-parse models) old_migration=$(git rev-parse migration) git rebase --onto main "$old_models" migration git rebase --onto migration main models git rebase --onto models "$old_migration" ui gh stack unstack gh stack init --base main migration models ui移动任何分支前先保存旧边界 SHA;其他重排用git log <old-parent>..<branch>识别每层范围,自底向上重放。
Exit 6:分支属于多个堆叠
当前分支无法唯一定位堆叠(通常是它是多个堆叠的 trunk)时退出 6,没有消歧旗标:
gh stack checkout <a-branch-unique-to-the-intended-stack>再重跑即可;带显式堆叠号的命令(merge 7、unstack 7)不推断当前分支,天然绕开此问题。
Exit 8:堆叠文件被锁
另一个gh stack进程持有.git/gh-stack.lock的独占锁,锁约 5 秒超时,等待后重试;持续退出 8 说明仍有进程持锁,先找到并停掉该进程。
Exit 10:被打断的 modify 会话
gh stack modify是 TUI-only,Agent 永远不应调用它。若仓库被他人留在此状态,执行gh stack modify --abort恢复。submit也会检测到 pending modify 状态,在 TTY 下询问是否用本地状态覆盖 GitHub 上的堆叠。
从其他工具或 worktree 驱动堆叠
当分支由 jj、Sapling、git-town、独立 worktree 等管理,本地.git/gh-stack文件会是错的或不存在时,用纯 API 的link:
gh stack link branch-a branch-b branch-c # 自底向上 gh stack link --base develop --open a b c # 非默认 trunk,直接可评审 gh stack link 10 20 30 # 按 PR 号 gh stack link 7 feature-d # 追加到已有堆叠 #7因为link不写本地状态,其结果上的本地导航命令(up、down、top、bottom)不可用;需要本地跟踪时用gh stack checkout <stack-number>。
硬约束与帮助命令的正确用法
从 SKILL.md 的 "Constraints" 汇总:
- 堆叠是严格线性的:一个父分支、至多一个子分支;并行工作用独立堆叠;
- 没有非交互的重排或删除;报错提示的
gh stack modify是 TUI-only——重组请走unstack+init; - PR 标题与描述是自动生成的,事后用
gh pr edit修改; checkout <branch-name>只在本地堆叠中解析;要从 GitHub 拉取堆叠必须用堆叠号或 PR 号。
帮助命令上有一个易错点:gh stack <command> --help才是旗标与参数的权威来源;gh stack help <command>不工作,只会打印顶层帮助。
三份参考文档按触发条件按需阅读,无需预加载:
- references/stack-design.md — 创建堆叠之前:层数、每层内容、是否另开堆叠的决策;
- references/commands.md — 命令异常失败时:前置条件、副作用、原子性与顺序保证;
- references/troubleshooting.md — rebase 冲突、squash-merge 之后、本地/远端分叉、堆叠重组、从其他工具驱动堆叠时。
小结:这份技能文档的写法本身也是范本
gh-stack技能文档示范了如何为一个交互式 CLI 编写"Agent 可执行"的操作规范:
- 概念先行:用一张方向图(
(main) <- auth <- api <- frontend)定义 trunk/底/顶/up/down 的词汇表,消除后续所有命令的方向歧义; - 交互式陷阱显式化:TTY 分叉行为不靠"你应该知道",而是给出"永远这样跑 / 永远别裸跑"的对照表;
- 机器可读协议:状态走
--json+ stdout,人话走 stderr,控制流只依赖退出码,并且每个非零退出码都绑定一个明确的恢复动作; - 诚实的边界声明:明确写出哪些操作没有非交互路径(
modify)、哪些是 TUI-only、哪些"退出码 0 但实际没做"(Sync aborted),让自动化流程能预判失败并选择替代路径。
在 Remotion 这样的 monorepo 中(包矩阵覆盖packages/下数十个包,跨包改动天然需要拆分评审),把"多部分工作 → 可评审的分支栈"固化为技能文档 + 三份按需加载的 references,正是把复杂 Git 工作流交给 Agent 安全执行的工程化方式。
【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考