你在 feature 分支上执行 git merge main,屏幕停在 halfway:
一个文件里出现了 <<<<<<<、=======、
>>>>>>> 三行冲突标记(conflict markers,
git 圈出「两边改了同一处、我自动合不了」的那几段文字的记号)。
此刻你手边有三条常见的坏路:闭眼选其中一边、把两边机械地都留下、或者
git merge --abort(中止,git 提供的「整个撤销、回到合并开始前」的开关)假装无事发生。
resolving-merge-conflicts 给的是第四条路:把每一次冲突当成一个意图问题而不是文本问题,
顺着 commit message、PR、原始 Issue 读清楚两边各自想干什么,
能兼顾就兼顾,不能兼顾就挑符合这次合并目标的一边、并把取舍说出口,
然后跑完项目的自动化检查,把这次 merge 或 rebase 干到完成。
它的 SKILL.md 全文只有五行编号指令,是全仓库最短的 skill 之一——
这节课的任务就是把这五行逐段拆开,讲清每一行背后的纪律、它会改动仓库里的什么、
以及行为不对时该拧哪一段文本。
0001 把 22 个已发布 skill 分成三层:配置层(跑一次性的初始设置)、编排层(你手动启动的完整流程)、
以及被反复调用的基本功。resolving-merge-conflicts 不在主 flow(idea → ship)的任何一步上,
它是一条 standalone(独立支线):docs 页的原话是
「a reach-for-it-anytime standalone」——随时需要随时伸手,用完把一棵干净、已提交的树还给你。
它是 model-invoked 的(人和 AI 都能启动):你可以手动敲
/resolving-merge-conflicts,AI 也会在检测到「正处在一次没合完的 merge / rebase 里」时
按 frontmatter 里的 description 自动加载它(判定依据见
.agents/invocation.md:
SKILL.md 的 frontmatter 没有 disable-model-invocation,
agents/openai.yaml 里也没有 policy 块,按默认规则即人和模型都够得着)。
它在整个系统里有一个独特的位置:它是 22 个 skill 里唯一一个以「完成一次 git 操作」本身为职责的。 其它 skill 的交付物是文档(spec、研究报告)、Issue tracker 里的 Issue、或者代码加测试; 这个 skill 的交付物是一次完成的合并——解好的文件、暂存好的状态、落好的 commit。 0001 的全表给它的一行是:什么时候用——「merge 或 rebase 已经冲突、进行到一半」; 会改什么——「冲突文件内容、stage 状态、完成合并的 commit;会跑项目检查并修坏掉的东西」; 干完接什么——「继续原分支的工作;禁止把 abort 当默认出路」。
resolving-merge-conflicts 这个名字——尽管它的
docs 页结尾写着
「不确定哪个 skill 合适时,ask-matt 会给你指路」。
这带来两个实际后果:第一,它的自动触发完全压在 frontmatter 的
description 那一句 “Use when you need to resolve an in-progress git merge/rebase conflict.” 上,
路由器帮不上忙;第二,如果你希望 ask-matt 覆盖它,要改的是
ask-matt/SKILL.md——仓库的 AGENTS.md 明确要求,
每当新增、改名、移除或改变一个 skill 在流程里的位置,都要回头同步这个路由器。
下一课(0019)正好讲 ask-matt 本身,可以带着这个问题去读。
罗盘:MISSION.md · 地图:0001 系统地图(全表中它的一行)· 启动方式判定:.agents/invocation.md · 路由器现状:ask-matt/SKILL.md
docs 页的一句话定义:它「work through an in-progress git merge or rebase conflict, hunk by hunk, and finishes the operation — resolved, checked, and committed」—— 逐个冲突块(hunk,文件里被冲突标记围起来的一段,是解冲突的最小单位) 地处理一次进行到一半的合并,直到这次操作被解完、查完、提交完。 这里有两个词先定义清楚,后面全课都要用:
| 启动方式 | 怎么发生 | 依据 |
|---|---|---|
| 人手动启动 | 在会话里敲 /resolving-merge-conflicts |
docs 页:「Type /resolving-merge-conflicts」 |
| AI 自动加载 | 任务落入触发条件(一次 merge / rebase 冲突进行中)时,模型按 description 自己把它拉进来 | frontmatter 的 description:「Use when you need to resolve an in-progress git merge/rebase conflict.」;docs 页:「or the agent reaches for it automatically when a task fits」 |
注意它没有兄弟参考文件:skills/engineering/resolving-merge-conflicts/ 目录下只有
SKILL.md(14 行)和 agents/openai.yaml(3 行,只有 Codex 选择器里的显示名和一句简介)。
没有模板、没有细则文件。全部行为规范就是那五行编号指令,第 4 节逐行拆。
| 情境 | 该用它吗 | 更该去哪 |
|---|---|---|
| merge 到一半,git 停在它自动合不了的冲突上 | 该,这正是它唯一的现场 | — |
| rebase 到一半,某个 commit 重放时碰出冲突 | 该,它会一直 continue 到所有 commit 过完 | — |
| 还没开始合并,想先规划合并的顺序和策略 | 不该,它不管「还没发生」的合并 | 这是普通的规划对话;想清楚了再动手合 |
| 合并早已完成,但之后行为不对、测试莫名失败 | 不该,冲突已经解完了,现场不存在了 | diagnosing-bugs(docs 页明确点名这个邻居) |
冲突太乱,想 --abort 撤销整个合并、退回起点重来 |
不该,而且这正是它明令禁止的出路 | 第 3 节讲为什么「中止」是诱惑而不是方案 |
| 不知道该走哪条流程 | 不该 | ask-matt(但注意第 1 节的提醒:它的地图里目前没列这个 skill) |
权威原文: skills/engineering/resolving-merge-conflicts/SKILL.md · 给人看的叙事版: docs/engineering/resolving-merge-conflicts.md (aihero.dev/skills-resolving-merge-conflicts)
这个 skill 的全部思想浓缩在 docs 页的一段话里:冲突的陷阱是把它当成一个文本问题—— 在冲突标记两侧里挑 “ours”(我方,当前所在分支的那一侧)或 “theirs”(对方,被合进来的那一侧), 只要让标记消失就算完事。而这个 skill 把它当成一个意图问题: 一个冲突块的每一侧之所以存在,是因为某个具体的人当时想要某个东西; 解冲突的义务是在能兼顾的地方同时保住两边的「想要」,在真的互斥的地方挑一边、并把代价说清楚。
三个词先立住,后面每一步都在用它们:
SKILL.md 第 2 步列了三样:
commit message(那次提交自己写的说明)、PR(合并请求页面里的讨论和描述)、
原始的 issue / ticket——按本仓库的统一词汇(CONTEXT.md),就是
Issue tracker(托管 Issue 的工具,如 GitHub Issues、Linear)里那条最早的
Issue(一个被跟踪的工作单元:bug、任务或规格)。
docs 页把因果关系说得很白:「You can't preserve an intent you haven't read,
so the work starts in the history — commits, PRs, tickets — not in the diff.」
(你保不住一个没读过的意图,所以活从历史开始干,不是从 diff 开始干。)
--abort——永远解到底,永不中止。中止把整个现场销毁,等于宣告「我读不懂这次合并」;这个 skill 的存在就是为了不让这句话成立。(顺带一个 git 本身的冷知识,不是这个 skill 的规定:rebase 中途的冲突里, “ours” 和 “theirs” 的指向和直觉相反——“ours” 是你正在往其上重放的基底, “theirs” 才是你正在重放的那串 commit。这正是「凭 ours/theirs 字眼猜」危险的原因之一; 按意图解就不需要赌方向。)
SKILL.md 的正文就是下面这五步,没有更多。表格里「原文」列是逐字引用,
「实际在干什么」列是把它落到一次真实冲突上的动作。
| 步 | 原文(逐字) | 实际在干什么 | 跳过这步的典型死法 |
|---|---|---|---|
| 1 | See the current state of the merge/rebase. Check git history, and the conflicting files. | 先看清现场:这是一次 merge 还是 rebase?卡在哪些文件、哪几个冲突块?git 历史长什么样?只看状态,还不动手改。 | 没搞清是 merge 还是 rebase 就动手,后面用错收尾命令;或者漏看某个冲突文件,把标记留在了仓库里。 |
| 2 | Find the primary sources for each conflict. Understand deeply why each change was made, and what the original intent was. Read the commit messages, check the PRs, check original issues/tickets. | 对每一个冲突,把两侧各自追回到一手来源:当初那个 commit 的说明、对应的 PR 讨论、最早的 Issue。目标是能用一句话说出「这一边想要什么」。 | 凭 diff 猜意图——猜错时解出来的代码语法没问题、语义是错的,而且这种错最难在评审里被看出来。 |
| 3 | Resolve each hunk. Preserve both intents where possible. Where incompatible, pick the one matching the merge's stated goal and note the trade-off. Do not invent new behaviour. Always resolve; never --abort. | 逐个冲突块动手解。先问「两个意图能不能都保住」;不能,就问「这次合并的既定目标是什么」,选与目标一致的一边,并把被放弃的那一边说出口。全程不发明新行为、不中止。 | 闭眼选 ours / theirs 让标记消失;或者写出两边都没有的「折中代码」;或者解到一半 --abort 跑路——三条红线各踩一条。 |
| 4 | Discover the project's automated checks and run them — typically typecheck, then tests, then format. Fix anything the merge broke. | 先去找这个项目自己的自动化检查(typecheck=类型检查,让编译器验证类型;tests=测试;format=格式化),按「类型检查 → 测试 → 格式化」的典型顺序跑;合并碰坏的任何东西,修到绿为止。 | 解完就提交,把「文本上没冲突、语义上是坏的」代码合进主干;格式化没过还会在别人的 CI 上炸回来。 |
| 5 | Finish the merge/rebase. Stage everything and commit. If rebasing, continue the rebase process until all commits are rebased. | 收尾。stage(暂存,git add 把文件放进待提交区)所有解好的文件并提交;rebase 场景要一次次 continue,直到整串 commit 全部重放完——中间任何一个 commit 再碰出冲突,就回到第 1 步对它重来一遍。 |
留一个「半完成的 rebase」在仓库里:后面的每个操作都会撞上这个未了状态,下一次冲突会叠在这次没解完的上面。 |
下面两个是示意场景(本课编的教学例子,不是仓库里的真实记录), 用来演示第 4 节那五行规则落到具体冲突上长什么样。
背景:feature 分支把函数 renderList(items) 改名为 renderItems(items, options),
并更新了全部既有调用;同一时期 main 分支新写了一处调用 renderList(rows)。
你在 feature 分支上 merge main,git 在那处新调用上停下——
它没法自己决定这行代码该长什么样。
按文本解(两种错法):
· 闭眼选 feature 一侧 → 新调用整个消失,main 上加它的人的意图被悄悄弄丢。
· 闭眼选 main 一侧 → 留下一个对旧名字的调用,typecheck 立刻变红。
按意图解(这个 skill 的做法):
第 2 步读到:feature 侧的意图是「改名并加 options 参数」,
main 侧的意图是「新增一处渲染调用」。
第 3 步判断:两个意图兼容 → 都保住:保留这处新调用,
但写成 renderItems(rows, { /* 按需填的 options */ })。
注意这不是「发明新行为」——没有哪个意图是新的,只是两个既有意图的合流。
背景:feature 分支把请求超时默认值从 30s 改成 10s(意图:快速失败,尽早暴露问题),
main 分支把同一个值改成 60s(意图:容忍慢网络,降低误报)。
同一行不能既是 10 又是 60——两个意图真的不兼容。
这个 skill 的做法(SKILL.md 第 3 步后半句):
· 看「这次合并的既定目标」(the merge's stated goal):
这次合并的目的如果是把 main 的稳定性修复带进 feature,就选 60s。
· 把取舍说出口(note the trade-off):在对话里写明
「采用了 main 的 60s;放弃了 feature 侧 10s 的快速失败意图,
那个意图需要在别处重新安排」。
· 不许做的:写成 35s 之类两边都不存在的「折中值」——
那是发明新行为,三条红线之一。
两个例子的对照就是这个 skill 的核心判断力:先穷尽「兼顾」,再谈「取舍」,永不走「发明」和「中止」。 5.1 里如果不做第 2 步的历史阅读,你根本不知道该把新调用改成新名字——diff 里看不出 renderItems 和 renderList 是同一个东西。
作者级理解的核心问题之一:这个 skill 跑完,仓库和外部系统里多了什么、少了什么? 它的副作用全部落在本地 git 仓库里,对外部系统(Issue tracker、PR)只读不写。
| 对象 | 会被怎么动 | 发生在哪一步 |
|---|---|---|
| 冲突文件的内容 | 改写:去掉冲突标记,写入按意图解好的内容 | 第 3 步 |
| 冲突文件之外的源码 | 可能改写:「Fix anything the merge broke」——检查跑红时,修到绿为止,修的地方不限于冲突文件本身 | 第 4 步 |
| git 暂存区 | 写入:解好的文件被 stage | 第 5 步 |
| git 提交历史 | 新增:merge 场景落一个完成合并的 commit;rebase 场景整串 commit 被重放完毕。这是它的交付物 | 第 5 步 |
| 项目的自动化检查 | 被执行:typecheck → tests → format;运行本身不改文件,但因修复而改文件是预期内的 | 第 4 步 |
| commit message、PR、Issue tracker 里的 Issue | 只读:作为一手来源被查阅,不被写入或更新 | 第 2 步 |
CONTEXT.md、ADR、docs、报告文件 |
绝不碰:它没有任何文档类交付物;词汇沉淀是 domain-modeling 的事,报告是别的 skill 的事 | — |
code-review(它能对着一个固定起点审查改动)再过一遍——
这是你自己的编排选择,不是这个 skill 内置的步骤。
先说依赖方向的两个事实,都是从仓库文本里能直接验证的:
SKILL.md 里没有出现
「Run the /xxx skill」式的 prose 调用(0001 讲过,那是本仓库 skill 之间互相拉对方进场的方式)。
它读 Issue tracker 里的 Issue,但那是「查一手来源」的阅读行为,不是调用 skill。
resolving-merge-conflicts,只有它自己的 SKILL.md、
所在 bucket 的 README 和它自己的 docs 页三处命中。
它是一个纯触发式的孤岛:进场的唯一通道是「你手动敲」或「模型按 description 自动加载」。
| 相邻 skill | 关系 | 出处 |
|---|---|---|
| diagnosing-bugs | docs 页点名的「natural neighbour」(天然邻居):合并解得干净、但之后行为不对,那是诊断问题,不是冲突问题——边界划在「合并是否已经完成」 | docs 页的 Where it fits 一节 |
| implement / tdd | 主 flow 的产出物(分支上的 commit)是它上游的「货源」:正是那些分支工作日后要合并,才制造它出场的现场 | 0001 的主 flow 结构 |
| setup-matt-pocock-skills | 间接相关:它配置好了 Issue tracker 的位置和约定,第 2 步「查原始 Issue」才知道去哪查 | ask-matt 的 Precondition 一节 |
| ask-matt | 理论上的路由器;但注意第 1 节的提醒——它的地图正文里目前没有为这个 skill 写条目 | ask-matt/SKILL.md 现状 |
邻居关系:docs/engineering/resolving-merge-conflicts.md · 地图:0001 · 相邻课:0015 diagnosing-bugs · 0011 implement · 0012 tdd · 0002 setup-matt-pocock-skills
diagnosing-bugs,
让它先建「一条命令就能复现这个 bug」的反馈回路再修。
code-review
对着固定起点过一遍(Standards + Spec 两个轴)。
这是可选的个人编排,不是流程内置步骤。
这个 skill 的全部可拧的地方加起来不到二十行文本,定位反而比大 skill 更容易: 症状对到某一步,就改那一步的那句话。
| 症状 | 先查哪里 | 不要误改 |
|---|---|---|
| AI 拿到冲突就闭眼选 ours / theirs,标记没了但语义坏了 | SKILL.md 第 3 步(preserve both intents / pick the one matching the merge's stated goal)和第 2 步(先读一手来源) |
docs 页的措辞(那是给人看的叙事,不驱动行为) |
| 解出来的代码里出现两条分支都没有的新逻辑 | SKILL.md 第 3 步的 “Do not invent new behaviour.” 一句 |
项目的测试文件(测试没病,是解冲突的纪律病了) |
冲突一多就 --abort 跑路 |
SKILL.md 第 3 步末尾的 “Always resolve; never --abort.” |
git 的别名和全局配置(管不住一个被 skill 明令禁止的动作) |
| 解完不跑检查就提交,CI 上才炸 | SKILL.md 第 4 步(discover checks;typecheck → tests → format 的顺序;fix anything the merge broke) |
CI 配置本身(先确认 skill 的纪律被执行了没有) |
| rebase 解了一个 commit 就停,留了个半完成的 rebase | SKILL.md 第 5 步(continue … until all commits are rebased) |
第 3 步(那一层管的是怎么解,不是有没有解完) |
| 模型在冲突现场从不自动加载它 | frontmatter 的 description 那一句 “Use when…”——model-invoked 的自动触发只认这段文字 |
plugin.json(它本来就在已发布数组里,不用动) |
| Codex 选择器里的显示名、简介不对 | agents/openai.yaml 的 interface.display_name / short_description |
frontmatter 的 name(那是技能标识符,不是展示文案) |
| 希望 ask-matt 能路由到它(目前地图里没有) | ask-matt/SKILL.md 的 Standalone 一节;改完按 AGENTS.md 的规则同步 docs | 本 skill 的 SKILL.md(被路由的一方不用改自己) |
| 改了 SKILL.md 的任何一步 | 按 AGENTS.md 的约定,重新同步 docs/engineering/resolving-merge-conflicts.md(人读文档跟着行为走) |
本课和速查表是教学副本,跟着一手材料再同步即可 |
先别往回翻表,凭记忆答。选项的长度刻意对齐,不会从版式泄题。规则以 SKILL.md 和 docs 页的原文为准。
--abort,这个 skill 的硬性规定是?--abort)。
然后回想你最近一次亲手解的冲突:如果按这五步重走一遍,
哪一步是你当时跳过的?跳过的那一步代价是什么?
本课主一手材料(请打开原文读,不要只背本页摘要——原文一共只有 14 行):
skills/engineering/resolving-merge-conflicts/SKILL.md
—— 全部行为规范:五步流程、三条红线。本课的首选 primary source。
docs/engineering/resolving-merge-conflicts.md
—— 给人看的叙事版:按意图解、「It's working if」四条验收、与 diagnosing-bugs 的边界
(aihero.dev/skills-resolving-merge-conflicts)。
…/agents/openai.yaml
—— Codex 侧元数据;没有 policy 块,印证它是 model-invoked。
速查页(本课同步): reference/resolving-merge-conflicts.html
导航: 上一课 0017 improve-codebase-architecture (并行编写中;那条线管「主动找重构候选」,和本课的「被 git 拦下来」是主动与被动的两端)。 总览仍回 0001 系统地图; 流程上的邻居见 0015 diagnosing-bugs。
建议下一课:
0019 ask-matt——那个把所有 user-reachable skill 画成地图的路由器本身。
带着这节课发现的问题去读它正好:为什么 resolving-merge-conflicts 不在地图里?
读完你就知道该往它的 Standalone 一节补一句什么。
SKILL.md 和 docs 页的原文,不会临场编造。
做完检索练习后,回复「练习结果 / 哪里卡住 / 开 0019 或先补 0015」,我们安排下一课。