Remotion 仓库 Agent 技能实战:用 gh-stack 管理堆叠分支与 Pull Request
2026/9/7 22:58:45 网站建设 项目流程

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-effectreleasepr等),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 让首次解决后的方案自动重放,这是堆叠工作流里最重要的体验优化;
  • checkouttrunk命令没有--remote参数,完全依赖remote.pushDefault配置;对pushsubmitsyncrebaselink而言,未配置remote.pushDefault时必须显式传--remote <name>,否则会出错。

非交互调用协议:Agent 使用 gh stack 的核心安全约束

这是整份技能文档中信息密度最高的部分。gh stack的许多命令以stdout 是否为 TTY作为行为分叉点:被管道化时,多数命令干净地报错或输出静态文本;而在 PTY 下,同样的命令会打开交互式提示或全屏 TUI,永久阻塞。由于不同 Agent 运行时(harness)对 TTY 的检测与模拟各不相同,文档明确要求:不要依赖这种检测,始终显式传旗标

"永远这样跑"对照表(来自 SKILL.md):

Always runNever run bare原因
gh stack view --jsongh stack viewPTY 下会打开 TUI
gh stack submit --autogh stack submit会为每个新 PR 提示输入标题
gh stack merge <target> --yes --squashgh pr mergegh 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/bottomgh stack switchswitch只有菜单模式
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没有非交互的就地重排,改顺序只能unstackinit

一个示意的 todo 应用分层(文档特别注明:这是举例,应从实际任务推断主题与层名,不要照抄):

(main) <- todo-app/models <- todo-app/api <- todo-app/frontend <- todo-app/integration
  • todo-app/models— 共享类型与 schema
  • todo-app/api— 使用 models 的路由
  • todo-app/frontend— 调用路由的组件
  • todo-app/integration— 覆盖整个功能的测试

要避免的典型失败模式:全写在一个分支上再试图事后拆分。如果任务大到值得用堆叠,就从一开始创建堆叠。

分支命名

推荐"共享主题前缀 + 层关注点"的形式:<topic>/<concern>,例如billing/schemabilling/apibilling/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)执行,否则以退出码5can 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的完整步骤(按序):

  1. Fetch远端;
  2. 与 GitHub 堆叠对账:在 github.com 上加入堆叠的 PR 会被拉下来并在本地追加;分叉时非交互模式中止;
  3. 快进 trunk(已是最新则跳过,分叉则告警);
  4. 必要时级联 rebase:trunk 移动、某个堆叠分支从远端快进、或某分支不再包含其预期父分支时触发;已合并 PR 自动处理;冲突时所有分支恢复到 rebase 前状态,退出码3
  5. 推送所有活跃分支(原子);
  6. 从 GitHub 刷新 PR 状态
  7. 同步堆叠对象——把开放 PR 链接进堆叠(只增),仅在存在两个及以上 PR 时;sync永不打开 PR,那是submit的事;
  8. --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 initgh stack checkout <target>
3Rebase 冲突见下文 Exit 3 恢复
4GitHub API 失败检查gh auth status,重试
5参数无效修正调用;参考<command> --help
6需要消歧分支属于多个堆叠;checkout 一个不共享的分支
7Rebase 已在进行中gh stack rebase --continue--abort
8堆叠文件被锁另一个gh stack进程在写;约 5 秒后重试
9仓库未启用 Stacked PRs告知用户
10Modify 恢复态gh stack modify --abort

Exit 3(rebase 冲突)恢复,区分两个来源:

  • gh stack rebase之后:解决文件、git add,然后gh stack rebase --continuegh 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 7unstack 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不写本地状态,其结果上的本地导航命令(updowntopbottom)不可用;需要本地跟踪时用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 可执行"的操作规范:

  1. 概念先行:用一张方向图((main) <- auth <- api <- frontend)定义 trunk/底/顶/up/down 的词汇表,消除后续所有命令的方向歧义;
  2. 交互式陷阱显式化:TTY 分叉行为不靠"你应该知道",而是给出"永远这样跑 / 永远别裸跑"的对照表;
  3. 机器可读协议:状态走--json+ stdout,人话走 stderr,控制流只依赖退出码,并且每个非零退出码都绑定一个明确的恢复动作;
  4. 诚实的边界声明:明确写出哪些操作没有非交互路径(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),仅供参考

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

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

立即咨询