关于 stacked-prs
Stacked PRs 是用于构建、发布、同步和验证依赖型拉取请求堆栈的 Skill。它将分支祖先关系与托管平台的审查基准分开建模,首个适配器文档覆盖 GitHub 的 gh,并将合并和清理交由 ship-workflow 执行。
主要功能
- 01检查分支与 PR 堆栈拓扑
- 02发布 PR 并修正基准分支
- 03使用预期旧值租约重排分支
- 04验证可审阅的堆栈切片
- 05通过提交尾注记录堆栈身份
- 06将合并与清理交给 ship-workflow
适用场景
- 01将依赖分支发布为 PR 堆栈
- 02父分支变更后同步堆栈
- 03检查堆栈拓扑与审阅准备情况
- 04准备经授权的堆栈以委托合并
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mathews-tom/ armory --skill stacked-prs安装后在 Claude Code 中输入/stacked-prs确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/cases.yamlreferences/github-native.mdmerge-discipline.mdmetadata-format.mdprovenance.mdprovider-adapters.mdstack-model.mdsync-algorithm.md详细介绍
堆叠 PR
构建、发布、同步并验证堆叠式拉取请求,避免破坏分支拓扑;同时确定合并现有堆栈所依赖的拓扑、来源和原生成员关系。
该软件包的身份与代码托管平台无关。Git 是分支祖先关系的事实来源;平台 PR 元数据是评审基准分支的事实来源。本文档首先介绍通过 gh 使用 GitHub 的平台适配器。
合并、合并后的提升和重新定位,以及清理现有手动或 GitHub 原生堆栈,都由同一个可执行主体负责:ship-workflow 技能的堆栈图运行。该技能负责撰写明确的堆栈决策数据包,并在 §5 中将其委托给此运行;它本身不提供可手动执行的合并或清理流程。
参考文件
| 文件 | 内容 | 何时加载 |
|---|---|---|
references/stack-model.md | 堆栈推断、明确排序、原生堆栈检测和明确的堆栈决策数据包 | 检查、发布、验证堆栈或委托处理堆栈时 |
references/provider-adapters.md | 平台适配器契约,以及用于列出、创建、重新定位和检查 PR 的 GitHub gh 命令 | 创建、重新定位或检查 PR 时 |
references/sync-algorithm.md | 切片证明、预期旧值租约和独立的重新堆叠流程 | 父分支或基准分支移动后同步堆栈时 |
references/merge-discipline.md | 按父级优先的合并规则、合并证明、保留策略,以及委托运行会强制执行的端点 | 决定选择哪些内容以及如何解读结果时 |
references/metadata-format.md | 可选的元数据架构和验证规则 | .stack-prs.yaml 存在或推断结果不明确时 |
references/provenance.md | 提交尾注中的堆栈身份、标记、验证及与合并模式的关联 | 创建、拆分、同步堆栈或选择合并方法时 |
references/github-native.md | 原生堆栈检测、关联资格、原生同步和原生前缀策略 | 请求或检测到 GitHub 原生堆栈时 |
适用场景
| 使用此技能 | 使用其他软件包 |
|---|---|
| 多个有依赖关系的分支需要针对父分支创建 PR | 单个独立 PR 使用 ship-workflow |
| 需要将功能分支拆分为便于评审的关联分支 | 代码尚未编写、需要规划任务时使用 task-decomposer |
| 现有堆栈需要变基、重新定位到正确的父分支、验证或确定合并范围 | 评审单个 PR 差异时使用 pr-review |
| 现有堆栈必须合并或在合并后清理(§5 中委托处理) | 清理无关分支时使用常规 Git 命令 |
核心规则
- 在任何工作流开始前运行
git rev-parse --show-toplevel。 - 在变基、推送、创建 PR 和更改 PR 目标分支前运行
git status --porcelain。 - 如果工作区不干净,则停止操作,除非用户明确要求仅进行检查。
- 优先采用现有 PR 的
baseRefName值,而不是推断祖先关系。 - 根据根 PR 的
baseRefName解析<base>(gh pr view <root-pr> --json baseRefName --jq .baseRefName),不要依据origin/HEAD;堆栈的主干分支不一定是仓库的默认分支。 - 如果父级推断存在歧义,请使用明确的分支顺序或
.stack-prs.yaml。 - 绝不使用普通的
git push --force或不带参数的--force-with-lease;推送重写后的分支头时,使用--force-with-lease=refs/heads/<branch>:<expected-old-oid>。 - 绝不手动删除堆栈分支;基于证据的清理应由委托运行完成(§5)。
- 为此技能创建或拆分的每个提交添加
Stack-Id和Stack-Position尾注;从.stack-prs.yaml复制 ID,若不存在则创建一个,并在之后保持不变。 - 在委托合并此技能创建的堆栈前,验证尾注已存在且彼此一致。
- 在作出任何合并决定前,Inspect 阶段必须探测每个打开的堆栈 PR,检查其是否属于原生堆栈(
references/stack-model.md§ 原生堆栈检测)。 - 在选择
squash作为某个节点的合并方法前,探测提供方的 squash 消息策略(squash_merge_commit_message)(references/provenance.md§ 合并模式耦合)。
GitHub 原生堆栈模式
GitHub 原生堆栈仅限同一仓库使用。它们是对手动 Armory 堆栈的增强,不会取代来源追踪尾注、.stack-prs.yaml 或与提供方无关的工作流。
原生堆栈成员身份是拉取请求在提供方一侧的属性,并非此技能选择的一种模式。在 Inspect 阶段、作出任何合并决定前,必须检测每个堆栈 PR 是否属于原生堆栈,无论是否请求了 github-native 模式:从 GET repos/{owner}/{repo}/pulls/<number> 读取 .stack,并从 GET repos/{owner}/{repo}/stacks/<stack-number> 读取完整的有序成员列表(references/github-native.md § 检测已存在的原生堆栈)。
创建原生状态(选择启用)
使用 gh stack link 将手动堆栈转换为原生堆栈属于提供方状态变更。只有在通过以下资格检查后才能执行:
- GitHub CLI 可用且
gh stack --help执行成功;如果缺少github/gh-stack,则安装它。 - 仓库已启用原生堆栈支持;原生堆栈退出代码 9 表示该堆栈仍保持手动模式。
- 每个拟用的头分支、基分支和拉取请求都属于同一个 GitHub 仓库;拒绝分叉仓库和跨仓库堆栈。
- 现有 PR 基分支、本地分支顺序以及
Stack-Id/Stack-Position来源追踪信息相符。若不一致,则停止操作。 - 用户明确请求使用原生模式。
如果检查未通过,堆栈将保持手动模式。绝不自动转换,也绝不在堆栈进行到一半时转换。
在已原生化的栈上操作(强制)
Inspect 一旦发现 PR 已原生化,原生处理就不是可选项:对于栈成员,提供方会拒绝同步合并;委派运行只会通过对所选最高成员发起一次受保护的异步请求来落地原生成员。上述条件 1-4 仍作为安全检查适用;条件 5 的同意不适用,因为提供方已注册该栈。
- 使用
gh stack link、gh stack sync、gh stack rebase和gh stack push进行链接与同步(references/github-native.md);每次非交互式同步后都要重新读取gh stack view。 - 若要落地,必须要求用户授权的确切最高 PR,并在栈决策数据包中选中其下方所有未关闭成员。提供方会合并直到该 PR 为止的整个未关闭前缀,因此授权范围之外的未关闭下层成员属于需要向用户确认的范围问题,绝不能隐式扩大范围。其上方的未关闭成员是数据包中未选中的节点。
- 根据 trailer 保留策略为每个选中成员选择合并方法(
references/github-native.md§ 选择合并方法以保留 trailer)。使用合并队列时,由队列规则选择方法,且不会发送数据包中的方法。 - 委派落地(§5)。原生成员没有逐成员请求、常规合并或手动回退;受保护的请求仅覆盖所选的最高 head(
references/github-native.md§ 落地策略)。 - 使用
squash_merge_commit_title: COMMIT_OR_PR_TITLE(GitHub 默认值)时,若 PR 只压缩一个提交,GitHub 会将该提交的标题作为基分支上的主题,而不是 PR 标题;合并后无法通过重写已推送的历史记录来更正。当可见主题很重要时,在委派前使未合并提交的标题与 PR 标题一致(git commit --amend -m "<pr-title>",然后执行git push --force-with-lease=refs/heads/<branch>:<old-head> origin <branch>),并在数据包中记录新的 head。
工作流
1. 检查
在不修改任何内容的情况下构建栈模型:
git rev-parse --show-toplevel
git status --porcelain
git branch --show-current
git for-each-ref --format='%(refname:short)' refs/heads
git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'
gh pr list --state open --json number,title,headRefName,headRefOid,baseRefName,state,url
为每个栈分支生成一行表格。记录此表格前,对每个 PR 运行原生栈探测(references/stack-model.md § 原生栈检测):
| 顺序 | 分支 | 父级 | PR | 状态 | 检查 | 原生 |
|---|---|---|---|---|---|---|
| 1 | feat/parser-core | main | #101 | 开放 | 待处理 | 否 |
| 2 | feat/parser-cache | feat/parser-core | #102 | 开放 | 待处理 | 否 |
如果没有可用的提供方适配器、没有本地分支与请求的栈匹配,或无法根据 PR 基础分支、显式顺序或元数据推断父级顺序,则停止。
2. 发布
创建缺失的 PR,并更正错误的基础分支:
git status --porcelain
git push -u origin <branch>
gh pr create \
--base <parent-branch> \
--head <branch> \
--title "<title>" \
--body-file <generated-body-file>
gh pr edit <number> --base <parent-branch>
生成的 PR 正文必须包含栈顺序和验证状态:
## Stack
Stack-Id: `auth-refactor-a1b2c3`
Base: `main`
Position: 1/3
1. `feat/parser-core` -> this PR
2. `feat/parser-cache` -> #102
3. `feat/parser-cli` -> #103
Depends on: (none - root)
Upstack: #102
## Validation
- Pending: commands not run yet
对于非根 PR,Depends on: 列出父 PR 编号。已知时,Upstack: 列出直接子 PR 编号。
若某分支在其父级之外没有提交、已有 PR 已关闭且未合并,或提供方拒绝更改基础分支,则停止。
3. 同步
在 <base> 或父分支移动后、任何内容落地前,使用 references/sync-algorithm.md 中的独立流程对开放分支重新栈化:记录每个子分支的旧 head C 和分叉点 F,在分离的临时工作树中将 F..C 重放到新的 tip L 上,证明结果正确,并使用显式的预期旧值 lease 发布。
git status --porcelain
git fetch origin
git merge-tree --write-tree --merge-base=<F> <L> <C>
git push --force-with-lease=refs/heads/<branch>:<C> origin <N>:refs/heads/<branch>
从基分支上方的第一个分支开始,朝叶节点方向继续。发生冲突、远程 lease 失败或父 PR 未合并便已关闭时停止。父级落地后提升并重新指定子分支的目标,是委派生命周期(§5)的一部分,而不是此步骤。
4. 验证
将堆栈拆分为可审查的切片进行验证。条件允许时,在每个分支上运行开销较低的检查;如果逐分支验证不切实际,则在叶节点上运行开销较高的完整检查。在每个 PR 正文中准确记录运行了哪些检查。
使用目标仓库检测到的本地门禁和提供方检查。操作其他仓库的堆栈时,不要运行 armory 软件包评估命令。
仅对本 armory 软件包实现,使用:
uv run python scripts/validate_evals.py
uv run python scripts/generate_manifest.py
uv run python scripts/evaluate_package.py --path skills/stacked-prs
5. 合入并清理(委派)
通过 ship-workflow 技能的显式堆栈图契约,一次性完成已有手动或原生堆栈的合入、合入后的提升和重新定向、合并后的验收与清理:
-
检查(§1),包括原生成员关系,并验证来源信息(
references/provenance.md§ Verifying Trailers)。 -
编写一份堆栈决策数据包(
references/stack-model.md§ Explicit Stack Decision Packet):仅对用户授权的 PR 设置select: true;将所选节点的每个祖先以及其他所有相关成员作为未选上下文;为每个所选节点填写不含修订版本的decision、引用的来源和合并方法;并填写provenance: {"disposition": "preserve", ...},注明检查过的内容。将其写入仓库外的私有临时文件。 -
加载已注册的
ship-workflow技能。如果客户端没有该技能,则以BLOCKED状态停止并注明ship-workflow;绝不要退而采用手动运行合并、重新定向、提升推送或删除。 -
从目标仓库的检出目录运行已加载技能的绝对基目录下的可执行文件,先以只读模式运行,再使用已授权的端点运行:
uv run --no-project --python 3.12 python "<ship-workflow-dir>/scripts/ship.py" --stack-plan "/tmp/stack.X/stack.json" --dry-run uv run --no-project --python 3.12 python "<ship-workflow-dir>/scripts/ship.py" --stack-plan "/tmp/stack.X/stack.json" --gates "/tmp/stack.X/gates.json" --until merged“合入堆栈”对应
--until merged;“合入并清理”对应默认的clean。单独的“清理”只选择提供方已报告为已合并的节点,因此运行会验证并清理这些节点,但不会合入任何新内容。 -
仅当控制器返回了已保存的运行时,才使用
--resume RUN_ID恢复;只有在决策已解决时,才添加拓扑相同的更新数据包。运行记录写入前的准入或检查停止可能会返回run_id: null或不返回 ID:解决其限定范围内的输入后,重新执行原先明确限定范围的调用;绝不要编造 ID,也不要使用不带参数的--resume选择无关运行。 -
转达运行状态、已确认的合并、保留的依赖项、清理处置结果和下一步操作。
NEEDS_JUDGMENT表示需要在此处处理拓扑、来源、范围或门禁决策;BLOCKED则指出缺少的前置条件。
该运行会强制执行父节点优先顺序、保留切片的提升、预期旧值租约、每个节点的新鲜门禁、开放基分支保留和合入证明(references/merge-discipline.md),并通过一次受保护的请求合入原生选择(references/github-native.md § Landing policy)。
常规委派式交付使用仓库当前的提供方检查、审查、保护规则和合并策略。所选节点可以明确请求 assurance: "high";常规节点无需固定检出日志或重命名 CI 作业。需要本地验证时提供 --gates;对每个重放的提交使用 per_commit: true,并为每个门禁设置各自的超时时间。验证在推送前进行,并保留现有钩子。输入未变时,对明确的拒绝复用完全相同的已验证重放对象;对不确定的变更,则先核对,再重试。已保存的保证级别和验证结果会在恢复运行时保留;不兼容的旧版授权会停止执行,并要求作出明确决策。
将一个分支拆分为堆栈
当一个源分支包含需要拆分为可审查且相互依赖的 PR 的功能时,使用拆分模式。要求用户提供源分支、基分支和目标分支顺序,或根据明确无歧义的提交名称确定这些信息。
先检查:
git status --porcelain
git fetch origin --prune
git merge-base <base> <source-branch>
git log --oneline --reverse <base>..<source-branch>
git diff --stat <base>...<source-branch>
git diff --name-status <base>...<source-branch>
选择最安全的拆分模式:
| 模式 | 使用场景 | 行为 |
|---|---|---|
| 提交范围拆分 | 连续提交可清晰地映射到各个切片 | 创建相互依赖的分支,并 cherry-pick 提交范围 |
| 提交列表拆分 | 不连续提交可清晰地映射到各个切片 | 按堆栈顺序 cherry-pick 指定的提交列表 |
| 补丁引导拆分 | 文件或代码块边界清晰,但提交内容混杂 | 变更前先暂停,等待用户批准拆分映射 |
| 拒绝自动拆分 | 变更在必要边界之间交错混杂 | 说明拆分不安全的原因 |
创建分支后,在发布前将末端分支与源分支进行比较:
git commit --amend --no-edit \
--trailer "Stack-Id: <stack-id>" \
--trailer "Stack-Position: <n>/<total>"
git diff --stat <source-branch>...<leaf-branch>
git diff --exit-code <source-branch>...<leaf-branch>
发布前,按照 references/provenance.md 为每个切片的提交添加尾注。添加尾注会修改提交消息,但不会改变树内容,因此末端分支与源分支的比较仍然是使用 git diff 进行的树比较,并且必须通过。
如果末端分支与源分支不同,则不要发布。
错误处理
| 情况 | 操作 |
|---|---|
| 变更前工作区不干净 | 停止并报告已变更的路径 |
| 父分支顺序不明确 | 请求明确的分支顺序或 .stack-prs.yaml |
| 存在已关闭且未合并的 PR | 停止,不要创建替代项 |
| 删除父分支后,已关闭且未合并的子分支 | 按照 references/merge-discipline.md § 恢复流程处理,然后委派剩余的合并工作 |
| rebase 或 cherry-pick 冲突 | 停止,报告分支和冲突文件,不要继续处理子分支 |
| 自上次 fetch 后远程分支发生变化 | 停止;未重新检查前不要重试 |
| 验证失败 | 记录失败的命令,并停止发布或委派 |
| 顶层拆分分支与源分支不同 | 停止创建 PR,并报告剩余差异 |
创建的堆栈中有提交缺少 Stack-Id 尾注 | 停止;按照 references/provenance.md 为未发布的提交添加尾注,然后再委派 |
尾注 Stack-Id 与 .stack-prs.yaml 不同 | 停止;变更前先确定规范 ID |
| 一个开放的下层原生成员不在授权前缀范围内 | 请求确认范围;绝不要扩大选择范围 |
请求合并或清理时未安装 ship-workflow | 停止并将状态标记为 BLOCKED,指出 ship-workflow |
委派返回 NEEDS_JUDGMENT 或 BLOCKED | 转达实际确认的事实,并在此处解决所指出的拓扑/来源记录/范围输入。仅恢复它返回的已保存运行;如果没有返回运行,则使用已解决的信息包重复原始限定范围的调用,绝不要单独使用 --resume。 |
输出格式
报告以下内容:
- 堆栈顺序,包括分支、父分支、PR 编号或 URL、状态和原生成员关系。
- 此技能执行的变更,包括创建 PR、修改基分支、rebase 和推送。
- 验证命令及准确的通过/失败状态。
- 对于委派的合并或清理,报告实际状态、已确认的合并、保留的依赖项、清理处置和后续操作;仅在返回且非 null 时包含
run_id。 - 下一步安全操作:修复验证问题、解决冲突、提供明确的分支顺序/范围,或恢复其返回的已保存运行。对于没有回执而停止的情况,解决问题后重复原始限定范围的调用。