Lesson 0018 · Engineering · 人和 AI 都能启动(model-invoked)· 流程课

resolving-merge-conflicts:按意图解冲突,把合并干完

你在 feature 分支上执行 git merge main,屏幕停在 halfway: 一个文件里出现了 <<<<<<<=======>>>>>>> 三行冲突标记(conflict markers, git 圈出「两边改了同一处、我自动合不了」的那几段文字的记号)。 此刻你手边有三条常见的坏路:闭眼选其中一边、把两边机械地都留下、或者 git merge --abort(中止,git 提供的「整个撤销、回到合并开始前」的开关)假装无事发生。 resolving-merge-conflicts 给的是第四条路:把每一次冲突当成一个意图问题而不是文本问题, 顺着 commit message、PR、原始 Issue 读清楚两边各自想干什么, 能兼顾就兼顾,不能兼顾就挑符合这次合并目标的一边、并把取舍说出口, 然后跑完项目的自动化检查,把这次 merge 或 rebase 干到完成。 它的 SKILL.md 全文只有五行编号指令,是全仓库最短的 skill 之一—— 这节课的任务就是把这五行逐段拆开,讲清每一行背后的纪律、它会改动仓库里的什么、 以及行为不对时该拧哪一段文本。

1. 它在整个系统里站在哪

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.mdSKILL.md 的 frontmatter 没有 disable-model-invocationagents/openai.yaml 里也没有 policy 块,按默认规则即人和模型都够得着)。

它在整个系统里有一个独特的位置:它是 22 个 skill 里唯一一个以「完成一次 git 操作」本身为职责的。 其它 skill 的交付物是文档(spec、研究报告)、Issue tracker 里的 Issue、或者代码加测试; 这个 skill 的交付物是一次完成的合并——解好的文件、暂存好的状态、落好的 commit。 0001 的全表给它的一行是:什么时候用——「merge 或 rebase 已经冲突、进行到一半」; 会改什么——「冲突文件内容、stage 状态、完成合并的 commit;会跑项目检查并修坏掉的东西」; 干完接什么——「继续原分支的工作;禁止把 abort 当默认出路」。

一个如实提醒:ask-matt 的地图里目前没有它 截至本课写作时,ask-matt/SKILL.md 的正文(主 flow、两条 on-ramp、standalone 清单、词汇地板)里 查不到 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

2. 它是什么、什么时候用

2.1 它是什么

docs 页的一句话定义:它「work through an in-progress git merge or rebase conflict, hunk by hunk, and finishes the operation — resolved, checked, and committed」—— 逐个冲突块(hunk,文件里被冲突标记围起来的一段,是解冲突的最小单位) 地处理一次进行到一半的合并,直到这次操作被解完、查完、提交完。 这里有两个词先定义清楚,后面全课都要用:

2.2 两种启动方式

启动方式 怎么发生 依据
人手动启动 在会话里敲 /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 节逐行拆。

2.3 什么时候用、什么时候别用

情境 该用它吗 更该去哪
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.mdaihero.dev/skills-resolving-merge-conflicts

3. 核心心智:按意图解,不按文本解

这个 skill 的全部思想浓缩在 docs 页的一段话里:冲突的陷阱是把它当成一个文本问题—— 在冲突标记两侧里挑 “ours”(我方,当前所在分支的那一侧)或 “theirs”(对方,被合进来的那一侧), 只要让标记消失就算完事。而这个 skill 把它当成一个意图问题: 一个冲突块的每一侧之所以存在,是因为某个具体的人当时想要某个东西; 解冲突的义务是在能兼顾的地方同时保住两边的「想要」,在真的互斥的地方挑一边、并把代价说清楚。

三个词先立住,后面每一步都在用它们:

三条不可谈的红线(SKILL.md 第 3 步原文)
  1. Preserve both intents where possible——能兼顾就兼顾,「选一边」是最后手段,不是默认动作。
  2. Do not invent new behaviour——不许发明新行为。解出来的结果里,不能出现两条分支上都不存在的逻辑;「写个折中实现让两边都闭嘴」恰恰是被禁止的那条路。
  3. Always resolve; never --abort——永远解到底,永不中止。中止把整个现场销毁,等于宣告「我读不懂这次合并」;这个 skill 的存在就是为了不让这句话成立。

(顺带一个 git 本身的冷知识,不是这个 skill 的规定:rebase 中途的冲突里, “ours” 和 “theirs” 的指向和直觉相反——“ours” 是你正在往其上重放的基底, “theirs” 才是你正在重放的那串 commit。这正是「凭 ours/theirs 字眼猜」危险的原因之一; 按意图解就不需要赌方向。)

4. 五步流程逐段拆

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」在仓库里:后面的每个操作都会撞上这个未了状态,下一次冲突会叠在这次没解完的上面。
五步其实是一根漏斗 第 1、2 步只读不写(看现场、读历史),第 3 步才第一次动文件,第 4、5 步验证并封存。 顺序本身就是纪律:读够了一手来源才配动冲突块,跑绿了检查才配提交。 行为出问题时,先问「是不是跳步了」——绝大多数走样都是第 2 步被跳过、或者第 4 步被省略。

5. 两个典型冲突的完整走查

下面两个是示意场景(本课编的教学例子,不是仓库里的真实记录), 用来演示第 4 节那五行规则落到具体冲突上长什么样。

5.1 意图兼容:改名撞上新调用

背景: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 */ })。
  注意这不是「发明新行为」——没有哪个意图是新的,只是两个既有意图的合流。

5.2 意图互斥:同一个默认值,两个方向

背景: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 是同一个东西。

6. 副作用清单:它会改什么、绝不碰什么

作者级理解的核心问题之一:这个 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 的事
它会替你 commit——这在全仓库的 skill 里是少数派 多数 skill 的终点是「把准备好的东西交给你确认」,而这个 skill 的终点是 「stage everything and commit」——它直接把合并提交落进历史。 所以第 4 步的「检查全绿」不是建议,是它替你按下提交键之前的最后一道闸。 如果你想在落 commit 之前多一双眼睛,可以把解好的 diff 先交给 code-review(它能对着一个固定起点审查改动)再过一遍—— 这是你自己的编排选择,不是这个 skill 内置的步骤。

7. 上下游:谁和它相邻

先说依赖方向的两个事实,都是从仓库文本里能直接验证的:

相邻 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

8. 用完之后接什么

  1. 正常结局:合并完成、检查全绿、commit 落好——回到你原来那条分支的工作上。 0001 全表给它的「干完接什么」就一句话:继续原分支的工作。
  2. 合并完成后发现行为不对:冲突已解完,现场不复存在,别再回到这个 skill—— 走 diagnosing-bugs, 让它先建「一条命令就能复现这个 bug」的反馈回路再修。
  3. 想给这次解冲突的 diff 再加一道审查:把合并前后的改动交给 code-review 对着固定起点过一遍(Standards + Spec 两个轴)。 这是可选的个人编排,不是流程内置步骤。
  4. 第 3 步里说出口的取舍别丢:被放弃的那个意图如果还重要, 它现在是一个已知欠账——值得变成 Issue tracker 里的一条 Issue 或一次明确的后续对话, 而不是留在你脑子里。

9. 行为不对时,去改哪个文件的哪一段

这个 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.yamlinterface.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(人读文档跟着行为走) 本课和速查表是教学副本,跟着一手材料再同步即可

10. 检索练习

先别往回翻表,凭记忆答。选项的长度刻意对齐,不会从版式泄题。规则以 SKILL.md 和 docs 页的原文为准。

自测(立即反馈)

1. 下列哪种情况是这个 skill 的正确触发时机?
2. 「按意图解,不按文本解」里的「意图」指什么?
3. 第 2 步说的 primary sources(一手来源)包括哪些?
4. 两边意图确实不兼容时,正确的做法是?
5. 「Do not invent new behaviour」禁止的是什么?
6. 关于 --abort,这个 skill 的硬性规定是?
7. 第 4 步跑项目自带检查时,文档给出的典型顺序是?
8. rebase 场景下,「完成」(第 5 步)指的是什么?
额外提取练习(无选项) 合上本页,默写五步流程(看现场 → 找一手来源 → 逐个冲突块按意图解 → 跑检查 → 收尾提交), 再默写三条红线(能兼顾就兼顾、不发明新行为、永不 --abort)。 然后回想你最近一次亲手解的冲突:如果按这五步重走一遍, 哪一步是你当时跳过的?跳过的那一步代价是什么?

11. 下一课与一手材料

本课主一手材料(请打开原文读,不要只背本页摘要——原文一共只有 14 行):

速查页(本课同步): reference/resolving-merge-conflicts.html

导航: 上一课 0017 improve-codebase-architecture (并行编写中;那条线管「主动找重构候选」,和本课的「被 git 拦下来」是主动与被动的两端)。 总览仍回 0001 系统地图; 流程上的邻居见 0015 diagnosing-bugs

建议下一课: 0019 ask-matt——那个把所有 user-reachable skill 画成地图的路由器本身。 带着这节课发现的问题去读它正好:为什么 resolving-merge-conflicts 不在地图里? 读完你就知道该往它的 Standalone 一节补一句什么。

老师就在会话里。 对本课任何一处有疑问——比如「merge 的既定目标」在真实仓库里该怎么认定、 意图兼容与「发明新行为」之间的灰色地带怎么划、rebase 中途再撞冲突时循环怎么转——直接在对话里问。 回答会回到 SKILL.md 和 docs 页的原文,不会临场编造。 做完检索练习后,回复「练习结果 / 哪里卡住 / 开 0019 或先补 0015」,我们安排下一课。