mcpField
D
debugkit

debugkit

github.com/mimukit开发2213 次安装今天更新
代码仓库

关于 debugkit

debugkit 是面向 Agent 的调试技能,将已观察到的故障转化为经验证的原因、失败复现和修复建议,但不会应用修复。其证据驱动流程以三种明确结果之一结束,也涵盖无法解释或复现的情况。

主要功能

  1. 01要求提供预期行为、实际行为和环境
  2. 02复现并缩减失败案例
  3. 03测试可证伪的原因假设
  4. 04通过开关测试验证原因
  5. 05撤销调查期间的仓库更改
  6. 06报告诊断但不应用修复

适用场景

  1. 01查找失败测试或接口性能回退的原因
  2. 02通过受控运行调查间歇性故障
  3. 03移交已验证原因和失败测试以供实现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mimukit/skills --skill debugkit

安装后在 Claude Code 中输入/debugkit确认已加载。

详细介绍

debugkit

当出现故障却没人知道原因时,就该用这个技能。其他构建技能都以意图为输入:计划、问题、差异或已确定的决策。debugkit 接收的是症状,它的全部任务就是将症状转化为可采取行动的原因。

这是一套单一流程,没有模式之分:每个 bug 都按相同方式执行整个流程,流程中的分支取决于证据允许得出什么结论,而不是用户提出了什么要求。

它只诊断,绝不修复

debugkit 会自由修改仓库以收集信息,并撤销所有这些修改。 日志行、二分查找、配置探测、注释掉的分支都可以使用,而且这些改动都不会保留。最终留下的是原因、失败的复现(红色测试会留在磁盘上,作为交接产物),以及一份描述修复方案而非实际应用修复的说明。

这条界限不是谦逊,而是报告可信的关键。一个既找到原因又实施补救的技能,已经对答案做出了选择,因此你之后读到的内容,不过是在为已经发生的修改作合理化解释。让诊断和修改由不同的人负责,中间就留出了让你提出异议的时机。

这个技能的工具中包含 Edit,正是因为探测会修改已跟踪文件。这使探测记录成为不可或缺的一环,而非装饰:它是调试过程与他人尚未提交的工作之间唯一的防线。

适用场景

“调试这个”、“为什么会失败”、“找出根本原因”、“这到底是什么导致的”、“这个测试不稳定”、“升级后就坏了”、“昨天还好好的,现在却不行了”、“这个端点变慢了”、“/debugkit”。

五条界限,每条一句:

  • 不是代码审查。 代码审查是检查刚写完的工作中的差异,以发现缺陷。debugkit 会追查已经运行过且行为异常的代码中的症状。差异中的代码如果尚未执行过,属于审查任务,而非调试任务。
  • 不是测试计划。 规划如何验证某项功能,发生在 bug 出现之前。debugkit 则从问题已经发生之后开始。
  • 不是原型设计。 一次性的试验用来回答尚未确定的意图(“这个设计能否经受考验?”)。debugkit 则通过探测来解释已观察到的故障。工具看起来相似,问题却恰恰相反。
  • 不是功能请求。 请参阅受理门槛。从未实现过的行为是缺失,而非损坏。
  • 不是优化。 性能回退属于适用范围:“它以前很快”意味着存在可供二分查找的变更,也有清晰的前后对比,因此流程无需修改即可运行。“让它更快”则不属于适用范围:它没有需要查找的原因,只有待查看的性能分析结果,而且下文的每道关卡都假定系统曾经正常运行,之后才停止正常工作。

受理门槛

流程开始前,需要掌握三项信息。若有任何一项缺失,只询问一次,然后等待补齐。

信息必要原因
预期行为本来应该发生什么
实际行为实际发生了什么
位置你在哪个环境、分支或机器上观察到了问题

预期与实际的差异就是症状。如果只告诉 Agent“它坏了”,Agent 就会自行编造预期,并据此进行调试;而这个编造出来的预期,正是所有自信地修错问题的根源。“位置”几乎不费力就能回答,还能立即决定本次运行是进入复现路径,还是进入仅收集证据模式。

这个门槛刻意设计成一句话就能回答,因为需要花十分钟才能跨过的门槛,最终只会被跳过。

只退回一次。 如果“预期行为”最终证明是从未实现过的行为,那么这属于功能请求,而非 bug。请说明这一点,并转交给规划技能:如果已安装 plankit,就使用它;否则请明确说明,这需要的是计划,而非诊断。

三种终止状态

每次运行最终都恰好落入其中一种状态。没有第四种;虚构第四种,正是这项技能要避免的失败。

结果含义后续去向
已证明原因开关测试通过,因此你可以随意切换症状的出现与消失交给构建技能,并附上失败复现
已复现,未解释Bug 可以稳定复现,且所有假设都已被排除返回给用户,并附上精简后的复现及已排除的候选原因
仪器测量计划完全无法复现 Bug返回给用户,并附上排序后的假设,以及能区分每种假设的测量方式

第四种状态是从未经过测试、听起来合理的理论;即便碰巧正确,它也依然是失败。 它读起来和真正的诊断毫无二致,信心、措辞和形式都一样,因此危险,而不只是没有帮助。

只有第一种结果会移交给构建步骤。其余两种都要交还给用户,因为没有任何内容得到证实,因此也就没有可供实现的内容。在这里,如实报告未得出结果是成功,而非不足。

流程

1. 复现

找到一条每次都能让它失败、任何人都能运行的命令。

门槛:没有复现,就没有诊断。 未复现的 Bug 所得出的理论,与实际发现无法区分;你无从判断自己写下的是哪一种。

这个门槛涉及的情况比表面看起来更多。所有仅在生产环境中出现的 Bug 都会卡在这里,所有无法安全切换的原因也一样,因为证明门槛只针对复现进行验证。如果症状只存在于运行中的系统,你就从未拥有安全验证任何结论的场所;这是复现失败,而不是证明失败。

当一条命令连续三次运行都能失败,或者你已声明症状是间歇性的,并将其带入证明门槛的统计形式时,这一步就算完成。

如果确实无法复现,请明确说明,并给出置信度,然后转入仪器测量计划分支。绝不要让它悄然成为备用方案;没有明确提示的降级运行,会被当作完整运行来理解。

2. 隔离

缩小复现范围,直到再移除任何内容都会让症状消失。产出是最小失败案例。

缩减时强制保证确定性。 固定随机种子,将并发操作序列化,冻结时钟,固定测试装置的顺序。一个 Bug 如果每二十次运行才失败一次,就无法切换它的出现与消失,因此确定性在这里不是可有可无的优化,而是通过下一道门槛的前提。

根据 Bug 实际涉及的维度进行二分:提交、输入、配置值、依赖版本,或两个环境之间的差异。

任何提交二分都必须在一次性工作树中进行,绝不能在用户的工作树里运行。 git bisect 会移动 HEAD,并要求工作树干净,因此在原处运行要么会直接拒绝,要么会覆盖用户未提交的工作以及你自己的探测记录:

git worktree add --detach <scratch-path> <known-good-ref>   # bisect lives here
git worktree remove <scratch-path>                          # before the run ends

如果已安装工作树技能,它负责管理这一生命周期;在这个生态系统中,该技能是 gitkit。如果没有安装,那么上面这两条命令就是全部流程。无论哪种情况,工作树都必须在运行结束前移除;如果移除失败,则必须在交接信息中注明其绝对路径。

**如果强制确定性后 Bug 完全消失,**这本身就是一项发现,因为时序就是作用机制。请明确说明,保留非确定性复现,并将其带入证明门槛的统计形式,而不是假装已经完成了缩减。

3. 提出假设

在测试任何假设之前,先写下可能的原因。 每个原因都包含一个可能被证伪的预测:你预期会观察到某种现象,而如果假设错误,就不会观察到这种现象。

没有可证伪预测的假设,只是披着标签的猜测。它无法被排除,因此会在整个排查过程中一直留存,到最后依然成立——这正是它最终出现在报告中的原因。

先写下假设也不是走形式。在探查之后才写出的假设,是根据你碰巧观察到的现象反向推导出来的,这样每个假设都能说得通,却没有一个能区分不同原因。

优先测试成本最低且最有区分力的假设,也就是能排除最多候选原因的假设,而不是最容易敲出来的那个。缩小一半范围,胜过确认你最喜欢的假设。

优先选择不会造成任何变更的探查方式。 直接调用单元、在全新进程中运行,或从外部打印中间值,通常同样有区分力;而不会写入内容的探查不需要还原,也不会丢失任何人的工作。只有当问题确实需要修改代码时,才使用探查记录表,不要把它当成默认选项。

当你再也无法写出带有可证伪预测的新假设时,就停止。 这就是终止条件,无需设定任意次数:想继续排查的 Agent 必须提出真实的预测,才能证明下一轮有必要。如果达到这个条件时,仍能复现错误,那么本次排查以已复现,尚未解释结束;这是一个真实的结果,而不是失败。

4. 验证

只接受开关测试,不接受更弱的证据。 只有在切换原因时也能切换症状,你才能确认原因:

  1. 存在原因 → 失败。
  2. 移除原因 → 通过。
  3. 恢复原因 → 再次失败。

缺少任何一步,都只是相关性。这是区分诊断与“我改了点东西,然后问题就没了”的唯一门槛。

引用这三步中每一步的证据。 如果门槛的输出只有“已确认”这个词,读者就无法核查你是否以任何可验证的方式运行了测试。

一次只做一项变更。 同时做两项变更,无法让你了解其中任何一项的作用;而这正是导致本技能旨在避免的错误的直接机制。

切勿针对生产环境、真实数据,或用户未指定的任何系统进行开关测试。 测试必须在复现环境中运行。这里无需为不安全情况设置例外,因为复现门槛已经筛出了这类情况:危险到无法切换测试的原因,应进入仪器化方案分支。这里的后门就像一扇标着跳过验证的门,恰好出现在最容易让 Agent 推门而入的情境中。

无法强制保证确定性时,开关测试应采用统计形式:分成三组,每组运行 N 次,分别测试存在原因、移除原因和恢复原因的情况,并报告每组的失败率。要做到诚实而非钻空子,有一条规则:运行前声明 N,绝不能运行后再声明。 真正的漏洞从来不是统计,而是不断运行,直到数字看起来足够有说服力。声明 N,报告每组的失败率,并说明使用了备用方案。

统计形式的证据门槛: N 至少为 20,移除原因组的失败次数为零,且存在原因组和恢复原因组各自至少出现 5 次失败。失败率为 25% 时,20 次运行都通过的概率约为 0.3%,因此移除原因组全部通过是有意义的。未达到这个门槛,就不能算作验证。应将结果报告为已复现,尚未解释,附上三组的失败率,并说明需要多大的 N 才能得出结论。

当三次开关测试或三组测试的证据都已引用且达到门槛时,这一步就完成了。

5. 报告

指出终端状态,然后提供所需内容。

已证实的原因。 用一句话说明原因、开/关状态的证据、失败的复现步骤,以及描述而非应用的修复方案。如果仓库中有测试运行器,就按仓库自身的测试布局将复现写成失败测试;否则写成命令。失败测试是构建技能所需的修复轮输入,因此无需翻译。

已复现,但未解释。 给出缩减后的复现、所有已排除的假设,以及排除每个假设的证据。这是一个未解释的 Bug 最有价值的产出:下一次尝试可以从范围小得多的问题入手,而不是从零开始。

检测计划。 按优先级列出假设,并为每个假设配上能够加以区分的具体实验或日志行。交付内容应回答接下来要测量什么,绝不能回答问题出在哪里。对未经测试的理论给出置信度百分比,正是复现关卡要防止的做法,只不过披上了数字的外衣,因此不要附上置信度。

探测台账

这项安全属性使自由修改成为可接受的做法。假设开始调试时用户有尚未提交的工作,因为他们通常确实如此,而且撤销探测绝不能撤销这些工作。

在首次探测前保存基线。 将台账放在工作树之外的目录中,这样其中的内容就不会出现在 git status 中。

snap=$(git stash create)    # an unreferenced commit of tracked changes; empty on a clean tree
git rev-parse HEAD                          > <ledger>/head.pre
git diff --binary --cached                  > <ledger>/index.pre
git status --porcelain=v1 --untracked-files=all > <ledger>/status.pre

git stash create 不会触碰任何引用、文件或索引,因此没有成本。记录它是否生成了对象。 在干净的工作树上,以及只有未跟踪文件发生变更的工作树上,它都不会输出任何内容,也就没有可恢复的快照。此时在台账中写入 none,并且绝不要为不存在的对象打印恢复命令。

快照不会涵盖未跟踪文件。如果探测可能触碰未跟踪文件(会编辑该文件,或运行会向其所在目录写入内容的命令),则在探测前将该文件复制到台账中。这份副本是该文件唯一的恢复途径。

将每次探测都记录为独立补丁,并针对该次探测前的状态生成。 每次探测前,将它会编辑的每个文件复制到台账中新建的探测目录中。探测后,将每份副本与对应文件进行差异比较。首次探测某个文件前,还要保留一份 .base 副本,以供最终检查。

cp <path> <ledger>/<name>.base                                # once, before the first probe on the file
cp <path> <ledger>/probe-NN/<name>.pre                        # before every probe
diff -u <ledger>/probe-NN/<name>.pre <path> > <ledger>/probe-NN/<name>.patch   # after it

每份补丁只包含该次探测的变更。若补丁是针对首次编辑时的副本生成的,就会包含之前所有探测的变更;反向应用这类补丁集合会重复撤销较早的探测,或导致失败。每次探测都要在台账中单独记录一行:路径、补丁,以及探测的原因。

按相反顺序反向应用记录的补丁来撤销变更。绝不要还原文件。

patch -R -s <path> < <ledger>/probe-NN/<name>.patch   # correct
git checkout -- <path>                                # banned, without exception

使用 patch -R,不要使用 git apply -R。 git apply 会根据补丁头中的路径,以仓库根目录为基准解析;而从树外快照生成的补丁所带的路径无法解析,因此它会报 invalid path,而不会应用补丁。patch -R 会针对你指定的文件应用补丁,并忽略补丁头,这正是你在此处需要的特性。

git checkout -- <path> 是一种条件反射式的做法,也是会悄悄破坏已有未提交编辑的做法。此禁令没有例外,包括基线时看起来干净的文件,因为一项需要临场权衡的禁令,很可能会在错误的时刻被你说服自己破例。

反向应用发生冲突时,立即停止并报告。 不要强行处理,也不要改用还原操作。说明文件名、探测内容和补丁位置,然后交由用户解决。

最后验证基线的全部三个部分。 以下各项检查全部通过,或已报告差异,才算完成清理:

  • 已跟踪内容。 每个被探测编辑过的文件都与其 .base 副本匹配(使用 cmp 检查),并且 git rev-parse HEAD 与 head.pre 匹配。
  • 索引。 git diff --binary --cached 与 index.pre 匹配。debugkit 从不暂存内容,因此任何差异都必须报告。
  • 未跟踪文件。 git status --porcelain=v1 --untracked-files=all 与 status.pre 匹配,但复现测试除外。每个被探测触碰过的未跟踪文件都与其台账副本匹配。

复现测试是唯一保留的写入内容。 它是已证实原因的交接产物,不是探测,因此不记入台账,也不会撤销。保持其失败状态且不要暂存,并使用绝对路径命名。若有其他有意保留的内容,也要用绝对路径报告,并且绝不要触碰本次运行未修改的文件。

每次运行都要在交接说明中打印基线。 有快照时使用第一种格式;git stash create 没有输出时使用第二种格式:

baseline snapshot: <sha> · recover with git stash apply <sha>
baseline snapshot: none (no tracked change at start) · probe patches in <ledger>

无人知晓的对象无法充当安全网,因为没有引用的提交无法通过 git fsck 找到。请明确说明,git 会按自己的时间安排清理不可达对象,因此这只是短期安全网,而非存档。不要创建引用:引用会在本次运行结束后继续存在。

Web 查询用于解读,而非诊断

允许查找信息,而且通常有必要。界限在于你如何使用答案。

  • 允许范围内。 翻译不透明的错误字符串、供应商状态码、不熟悉的库中的堆栈帧,或你刚通过二分定位到的版本的更新日志条目。解读你已经观察到的信号。
  • 超出范围。 从博客文章中寻找假设,或应用某个问题讨论串中提到的有效修复方法。这是别人对别人的 Bug 做出的诊断。

在可复现分支中,这一规则会自行生效:无论想法来源是什么,都必须进行开关测试,因此未经测试的建议无论来自何处,都无法进入报告。

在仪器化计划分支中不会进行任何测试,因此其约束方式是:每条记录都必须注明能区分假设的实验,否则就删除。 从问题讨论串中照搬的修复没有附带实验,因为它说的是这样做,而不是测量这个,因此这项要求会从结构上将其筛除,无需任何人记住关于信息来源的规则。

文档产物

按结果决定,而不是按排查过程有多艰难来决定。

结果是否需要文件?
已复现,但未解释始终需要
仪器化计划始终需要
已证实原因,且原因不在代码中(环境、配置、数据、依赖版本)始终需要
已证实原因,且原因在代码中不需要,在报告中内联说明

规则已经说明了自身的理由:文件用于记录提交历史无法体现的内容。 代码中的已证实原因已由修复及其测试完整记录,因此文档会重复这些内容。过时的环境变量和一系列已排除的假设则没有其他记录,而这些正是没人会在下个月记得的内容。

将文件写入 docs/debug/NNNN-debug-<slug>-YYYY-MM-DD.md,名称由四位数字序号、小写类型前缀、简短的小写 kebab-case 主题别名,以及末尾的 ISO 创建日期组成。要获取序号 NNNN,请检查 docs/debug/ 中当前工作树、git worktree list 列出的所有其他工作树,以及 origin/HEAD(在 git fetch origin 后执行 git ls-tree --name-only origin/HEAD docs/debug/)里最大的四位前缀序号,并加一;如果没有,则从 0001 开始。请扫描这三个位置,否则并行工作树可能会使用同一序号。如果某个来源检查失败(例如没有远程仓库),则跳过该来源;如果没有 git,则只列出目录。序号按目录独立计算,绝不重复使用。编辑文件时保持整个文件名稳定;再次处理同一个 Bug 时,应原地更新同一文件,而不是另建副本。如果仓库已有固定的事后复盘目录或命名规范,则以该惯例为准,并说明你遵循了该惯例。

文件必须持久保存并可提交:事后复盘是供日后阅读的内容,应纳入版本控制,而不是放在临时目录中。debugkit 仍然绝不提交该文件。

没有可写文件系统(例如基于浏览器的 Agent)→ 在规范路径下以代码块形式输出该文档,并跳过写入。不写入文件也是合法结果,无需道歉。

交接

本节使用程序性语体:每句话只写一条指令,使用主动语态和现在时,不使用比喻。

变更内容。 指明终止状态。列出你执行的每项探测,并确认已撤销。报告每项基线检查的结果。说明你未应用任何修复。通过绝对路径指明复现测试,并说明它仍保留在磁盘上,状态为失败且未暂存。通过绝对路径指明你有意保留的任何文件。通过绝对路径指明你未能删除的任何 bisect 工作树。

结果位置。 给出产物路径,或者说明该结果不需要文件。按探测记录选定的格式,每次运行都打印基线行:

baseline snapshot: <sha> · recover with git stash apply <sha>
baseline snapshot: none (no tracked change at start) · probe patches in <ledger>

后续步骤。 选定一项后续操作,并使之与结果相匹配:

  • 已证实的原因 → 将复现测试和所述修复交给构建技能。若已安装 implementkit,请指定 implementkit,并将其作为修复轮次,输入为复现测试:implementkit fix round: make <test path> pass; cause: <one sentence>。implementkit 会先运行失败的测试、保留该测试,并在完成门槛处展示测试通过。否则,请明确说明下一步是编写修复、保留测试并观察测试通过。
  • 已复现,未解释 → 告诉用户需要哪些证据才能重新开始排查。不要将其交给构建技能。目前尚无任何结论得到证实。
  • 检测方案 → 告诉用户部署你列出的测量手段。说明证据到位后应再次运行 debugkit。

不要自行开始下一步。

备注

  • 绝不应用修复。 不附带条件,不为单行代码破例,即便修复显而易见也不例外。这条规则在单行修复时最有价值,因为那时跳过它似乎无伤大雅。
  • 绝不把未通过的门槛说成已通过。 未证实的原因就报告为未证实。跳过开关测试的运行并未找到原因,无论它发现了什么。
  • 转交,不启动。 指明下一项技能及其单行调用方式;不要调用它。是否根据诊断采取行动,由用户决定。
  • 转交,不强求。 如果这里提到的技能不存在,每条建议都应退化为一项普通操作。在只有 git、别无其他内容的纯净仓库中,debugkit 依然有用。
  • 以仓库约定为准。 仓库中既定的事后分析文档位置、已记录的惯例,或 Agent 指南文件(CLAUDE.md 或等效文件)中声明的策略优先,因此要说明你遵循了该约定。

评论

支持 Markdown