关于我
AI 与开发
Harness Engineering 实践:构建 AI 自治的代码腐烂防御体系

Harness Engineering 实践:构建 AI 自治的代码腐烂防御体系

用 AI 把维护工作做成常驻后台进程,7×24 对抗代码库的自然退化。本文给出调度、选包、质量门禁和反馈回路的实现地图,以及一段可直接发给 Claude Code 的初始化 Prompt。

代码库的熵增是软件工程不可抗逆的规律。随着项目规模扩大、参与人数增多,any 类型悄悄扩散,被注释掉的 eslint-disable 越堆越多,依赖版本停滞在两年前,dead code 散落在每个角落。这不是开发者的失职,而是一种结构性问题——每个人都知道这些事情应该做,但在有需求压力的情况下,没有人会把它们排进 sprint。

这篇文章描述的是另一条路——用 AI 把维护工作做成一个常驻后台进程,7×24 不间歇地对抗代码库的自然退化。在 Coze Monorepo 中,这套体系已经稳定运行,完成了 144 次调度,产出 192 个 commit,净改动约 8 万行代码,整个过程完全无人值守。从零搭建花了大约一天,此后新增一类任务只需十几分钟。

读完这篇文章,你能拿到的是:一张足够具体的实现地图,知道调度、选包、质量门禁和反馈回路分别该放在哪里;以及文末一段可以直接发给 Claude Code 的初始化 Prompt,帮你在自己的仓库把这套体系跑起来。


一、先看效果

在谈架构之前,先把结果摆出来,因为这套体系的价值需要用时间维度来理解,而不是单次跑通的效果。

指标数值
运行频率CI 定时触发,全年无休
任务类型9 种工程优化任务并行调度
运行次数144 次,产出 192 个 commit
代码改动累计 2028 个文件,净增删约 8 万行
单次耗时20–60 分钟(含全量质量检测)
自愈能力质量检测失败后平均 1–2 轮自动修复通过

192 个 commit 分布在 9 类维护任务上,涉及 2028 个文件——这里的"净"很重要,意味着删除的 dead code、精简的类型标注贡献了相当大的比例,不是简单地添加内容。单次执行耗时在 20-60 分钟之间,包含完整的质量检测(TypeScript 类型检查、ESLint、build 验证)。成功的任务提正式 MR,失败的提 Draft MR 并附带详细的失败日志。

这组数据意味着什么?粗略估算,假设每次人工执行相同工作需要 2 小时,144 次就是 288 小时,折合大约 36 个工作日。更关键的是,这 36 天的工作不是一次性做完的,而是以每天多次的频率持续进行的,每次都只改一个 package,每次都经过严格的质量验证。这种节奏是人工操作很难复现的。


二、核心执行框架

2.1 整体架构

整个体系由四个主要部分组成:触发层(CI 定时任务)、调度层(harness-dispatcher)、执行层(各类 Skill)以及审计层(harness-audit)。它们之间的关系不是简单的串行流水线,而是一个带反馈回路的闭环。

harness-dispatcher — 产出新 MR rebase-mr — 维护存量 MR 通过 失败 CI Cron(定时触发) 👤 人工 review MR 解析基准分支 加权随机选任务读 tasks.md,综合历史 + open MR 负载 创建工作分支chore/harness-<task>-<date> 执行任务按 prompt 调用执行层 skill 质量检测循环自动修复,最多 5 轮 检测通过? commit & push 创建正式 MR 创建 Draft MR 写入运行日志 对所有 open harness MR执行 rebase,保持随时可合入

调度层和执行层是分离的——dispatcher 只负责"选什么任务、在哪个包上执行",执行层的 Skill 只负责"把这个任务做完"。这种分离让每类任务可以独立迭代,加一个新任务类型不需要改调度逻辑。所有具体的代码优化能力(删死代码、清依赖、修类型……)都封装在独立的 Skill 里;任务注册表(tasks.md)把这些 Skill 组织成一个个可调度的任务;harness-dispatcher 作为中央调度器,定时被 CI 触发后,从注册表里加权随机选一个任务,拉起对应 Skill 执行,执行完经过质量门禁验证,最终产出一个 MR。

最后,再通过审计层的 harness-audit 不断分析体系自身的运行数据,反过来调整注册表权重和 Skill 的执行策略,让整条流水线越跑越准。这一层不是可选的附加组件,而是闭环的必要部分——少了它,dispatcher 的配置在初始那一刻就被冻结,代码库的状态却在持续变化:某类问题被修干净了,dispatcher 还在徒劳地选这类任务;某类任务的成功率掉到 30%,权重却没有任何下调,CI 时间大量浪费在注定失败的任务上。有了审计层之后,运行日志被 harness-audit 消化、汇总、再写回 tasks.md,整套体系才真正具备了自迭代能力。

2.2 Skill:体系的基础单元

执行层的 Skill 按职责分成五个层次,每层对应不同的抽象级别,相互之间通过明确的输入输出衔接:

层级代表 Skill职责
调度层harness-dispatcher读 tasks.md,加权随机选任务和目标包,拉起执行 Skill,处理结果
分析层repo-lens扫描代码库,按任务条件计算各包"就绪分数",输出候选包列表
执行层dead-code-cleanerdep-cleaneroptimize-any-types……每类任务一个 Skill,只负责改代码,不做任何质量检测
验证层harness-validator跑 TypeScript 类型检查、ESLint、build 验证,失败时触发自愈循环
元层harness-audit独立于执行链路之外,定期读运行日志,分析模式,回写 tasks.md 的权重和配置

这种分层的直接好处是:每层 Skill 的输入输出非常确定,AI 在执行时不需要理解整个系统,只需要完成自己那一层的任务。调度器不内置任何优化逻辑,它只负责"选谁、何时运行、结果怎么处理";具体怎么改代码,完全由执行层 Skill 决定。新增一类维护任务时,绝大多数情况下只需要写一个新的执行层 Skill,其它四层都不用动。

2.3 任务注册表(tasks.md)

任务注册表(tasks.md)是整个体系的配置中心,每个任务用一个 HTML comment 块声明(调度器通过解析这些注释块来读取任务配置):

<!-- task
name: dead-code-cleaner
description: 找出候选包,删除无引用的死代码
prompt: |
  1. 运行 /repo-lens,获取仓库候选包推荐列表
  2. 运行 parse-history.js,排除近 30 天已处理过的包
  3. 从剩余候选中选取推荐分最高的包
  4. 运行 /dead-code-cleaner <selected-package> --yes
weight: 10
enabled: true
-->

三个字段的含义:

  • prompt:调度器直接交给 AI 执行的指令,自然语言,可调用任意 Skill。
  • weight:决定被选中的概率,实际运行时还会结合近期执行历史和 open MR 负载动态折扣。
  • enabled:设为 false 时不参与选取,用于临时下线某个任务。

把任务做成配置而不是代码,调整起来就很轻:调权重直接改 weight、临时下线改 enabled: false、新增任务追加几行自然语言,调度器代码一行不用动

目前已注册 9 个任务:

任务作用
dead-code-cleaner删除无任何引用的死代码文件
dep-cleaner删除 package.json 中未被源码实际引用的冗余依赖
optimize-any-types消除 TypeScript any 类型,补充精确类型声明
fix-eslint-disable删除 eslint-disable 注释,修复根因(白名单规则跳过)
fix-ts-expect-error删除 @ts-expect-error 注释,修复底层类型结构
add-tests为测试覆盖不足的包补充单元测试
update-deps升级非关键外部依赖(跳过 react、typescript 等基础包)
upgrade-rush-pnpmRushpnpm 升级到 npm 最新版本
doc-sync检测 AGENTS.md、ARCHITECTURE.md 等文档与代码的偏差,原地更新

2.4 快速注册新任务:harness-register

将一个新的维护能力接入 harness 的成本极低,整个过程分两步。第一步在本地完成 Skill 开发:把想做的事情(比如"把所有用 var 写的变量改成 const/let")描述给 Claude Code,让它生成对应的 Skill 文件,然后在一两个 package 上手动跑一遍,确认行为符合预期、不会误改文件。第二步把这个 Skill 接入调度器:运行 /harness-register <skill-name> --weight <n>,注册器会读取 Skill 的定义和参数格式,判断任务粒度(Package 粒度还是 Repo 粒度),自动生成标准 prompt(包含 repo-lens 选包、历史去重逻辑),最后追加到 tasks.md从"我有一个想法"到"它在 CI 里自动跑起来了",全程不到半小时。

新增任务不需要改任何调度代码,这正是把任务定义成 Prompt 而非硬编码逻辑的直接收益——任务的"做什么、怎么做"完全由自然语言描述,调度器只是个解释器,加任务的边际成本被压到了最低,"扩展任务边界"从一个工程项变成了一个日常操作。

2.5 候选包选取:repo-lens

harness-dispatcher 在每次触发时要回答两个问题:做什么任务,在哪个包上做。这两个决策都不是确定性的,而是带权重的随机选择——确定性策略容易陷入"总是改同一个包"的死循环。

候选包选取由 repo-lens 完成。它遍历 monorepo 中所有 package,对每个包计算一个"就绪分数":对于 dead-code-cleaner,就绪分数取决于未使用导出的数量;对于 optimize-any-types,取决于显式 any 的数量;对于 add-tests,取决于测试覆盖率缺口。就绪分数超过阈值的 package 才会进入候选池。核心思路是代码量大、近期改动少、被依赖少的包,清理收益高且风险低,优先处理。

此外,还有一个重要的过滤条件:排除最近被改动过的 package。如果某个 package 在过去 N 天已经被 harness 处理过,它会从候选池中移除。这个设计避免了 AI 反复改同一个 package 而其他 package 没有覆盖到的问题,同时也给 MR review 留出了时间窗口——万一上一个 MR 有问题,在它被 review 之前不会继续叠加改动。

2.6 加权随机 + 负载均衡

任务选取的加权随机逻辑相对简单:从 tasks.md 读取所有启用任务的 weight,按权重做随机抽样。在此基础上有两层负载均衡:7 天内执行过的任务权重减半(历史折扣),open MR 越多的任务类型权重越低(负载折扣),但每个任务始终保留至少权重 1,确保始终有被选中的机会。harness-audit 会定期根据成功率进一步调整权重——成功率高的任务权重提升,失败率高的任务权重降低,直到积累足够的成功案例再考虑恢复。

实际效果是任务选择分布与配置权重基本吻合,不会出现某类任务长期垄断或某类任务长期被冷落的情况。


三、质量门禁与自愈机制

执行 Skill 完成代码修改后,控制权交给 harness-validator。验证采用两阶段策略:第一阶段只跑受影响的 package,快速发现问题(约 45 秒);通过后进入第二阶段,全量 install + test + ts-check + lint + build,慢但全面(10-30 分钟),兜底保障。两个阶段任意一个失败,都会触发自愈循环。

自愈循环的逻辑是:将失败的错误信息作为新的上下文,请 AI 修复问题,然后重新验证,最多执行 5 轮。选择 5 而不是更大的数字,是因为经验表明能在 3 轮内修复的问题基本上是可修复的,3 轮以上还修不好的问题往往意味着任务设计本身有问题,继续尝试成本很高且成功率极低。另外,循环中还引入了"不得回退已通过的检查"约束:每轮开始前记录当前通过的检查,结束后验证这些检查是否仍然通过,如果出现回退,立即终止循环——这是防止"修 A 引入 B"死循环的关键。

WHILE 迭代次数 <= 5:
  运行 quality-check(增量检测 → 全量检测)
  IF PASS → 进入提交
  ELSE:
    计算错误指纹
    IF 同一错误出现 2 次 → 触发断路器,停止修复
    ELSE → 根据 FAILED_STAGE 定向修复(tsc / lint / build)

5 轮全部失败后,体系不会静默丢弃这次执行,而是将当前工作区的改动提为 Draft MR,同时把完整的运行日志(包括每一轮的错误信息、AI 的修复尝试、验证结果)作为 MR 描述附上。即使最终检测没过,也不是浪费——Draft MR 记录了本次的执行详情和失败原因,没合入也是低成本试错,不是沉默的失败。这些失败日志也是 harness-audit 的主要数据来源——从失败中学习比从成功中学习往往更有价值。

还有一个边界情况:若错误在 base branch 上已存在,不归因到本次改动,创建 Draft MR 供人工判断,不因他人遗留问题阻塞本次任务。


四、harness-audit:自迭代闭环

harness-audit 是这套体系与一般自动化脚本的本质区别所在。一个普通的自动化脚本在初始配置之后就固化了,它的效果会随着代码库状态变化而漂移——某类问题被修完了,但脚本还在徒劳地搜索;某类新问题出现了,但脚本没有能力发现。

harness-audit 不只是出一份分析报告给人看,而是会直接改配置:定期读取累积的运行日志,先把基础设施类失败(API 500、网络超时、鉴权问题)过滤掉,再针对任务本身的问题决定怎么调权重、调参数:

观察到的现象自动执行的动作
add-tests 跳过率 >50%降低 weight,缩短历史排除窗口(30 天→14 天)
rush test:cov 失败中 80% 是预存失败dispatcher 修复策略新增预存失败快速识别逻辑
平均修复轮次经常逼近上限且成功率尚可提高迭代上限(5→7),给 AI 更多修复机会
某个 skill 频繁因"找不到测试框架"失败在该 skill 的 SKILL.md 补充预检步骤

触发 harness-audit 的 Prompt 如下,可以直接发给 Claude Code 运行:

读取 docs/harness/ 目录下最近 N 条运行日志,分析以下指标:

1. 每个任务 ID 的执行次数、成功次数、失败次数及失败原因分布
2. 每个任务的平均耗时(分钟)
3. 失败案例中最常见的错误类型(TypeScript 错误、ESLint 错误、build 错误)
4. 自愈循环在第几轮通常能解决问题(分布统计)

基于以上分析,更新 tasks.md 中各任务的 weight 字段:
- 成功率 > 80%:weight 保持或小幅提升(+2)
- 成功率 60%-80%:weight 不变
- 成功率 < 60%:weight 降低(-5,最低为 1)
- 连续 3 次全部失败的任务:weight 设为 0(暂停)

同时在日志目录生成 audit-YYYY-MM-DD.md,包含完整分析过程和权重调整依据。
若有效记录少于 3 条,仅生成记录报告,提示样本不足。

运行得越久,体系越聪明。harness 的优化对象不只是"仓库代码",还包括自身的任务配置、检查策略和 Skill 实现。


五、已注册任务详解

能进入这套闭环的任务,需要同时满足几个关键条件:问题定义清晰(不需要理解业务上下文)、验证方式客观(机器说过就是过)、改动范围可控(单个 package)、业务耦合低。反过来,需要业务理解的重构、涉及运行时行为的改动、跨包架构调整,不在 harness 的范围内。以下逐一说明目前注册的 9 个任务:它们分别在解决什么问题、Skill 的具体做法、对 repo-lens 评分的影响,以及需要 reviewer 重点关注的风险。

5.1 dead-code-cleaner

随着业务迭代,组件被重构、功能被下线,遗留的源文件既没有被 import,也没有出现在构建入口或 package exports 里,但始终躺在仓库里。放着不管,tsc 和 build 的扫描范围随文件数增长而扩大,冷启动越来越慢;新人看到这些文件会误以为仍在使用,造成理解负担;偶尔还会被误引用,触发难以追踪的 bug。

Skill 的做法是用 knip 扫描指定 package,识别所有未被引用的文件;对每个候选文件做二次确认,排除动态引用、test fixture 等特例;批量删除后运行 tsc + build 验证,失败则逐文件回滚直至通过。执行完成后,代码量(loc)下降,包的 repo-lens 评分相应降低,同一个包短期内不会再被选中。

这是 9 个任务里最安全的一类,也是建议最先接入 harness 的原因。需要留意的是动态 import() 和运行时路径拼接无法被静态分析识别,knip 可能漏判;此外,monorepo 内部 knip 能覆盖跨包引用,但跨仓库的路径引用需要人工确认。

生成此 Skill 的 Prompt:

帮我创建一个 dead-code-cleaner skill,用于扫描并删除指定 package 中无任何引用的死代码文件。

执行流程:
1. 确认 knip 配置是否存在,不存在则生成临时配置
2. 运行 knip,收集所有"未被引用的文件"列表
3. 对每个候选文件做二次判断:排除 test fixture、.d.ts 声明文件、动态路径拼接引用
4. 输出候选列表,批量删除
5. 运行 tsc + build 验证,失败则逐文件回滚,找出误删项
6. 输出删除文件数量、验证结果

要求:全程不需要用户确认,支持 --yes 参数跳过交互;删除前做 git 快照便于回滚。

5.2 dep-cleaner

package.json 里声明的依赖,未必真的被源码引用。依赖会随需求增删,但删除往往被遗忘,node_modules 里装着一堆没人用的包。累积下去,rush installrush update 耗时随无用依赖增长;lock 文件膨胀,CR 噪音增加;安全扫描会对未使用的依赖也产生告警,掩盖真实风险。

Skill 的做法是静态分析源码中所有 import / require 语句,提取实际使用的包名集合,与 package.jsondependencies / devDependencies 做差集;对差集中的每个包进一步确认,排除 peer dependency、构建插件、类型包间接引用等情况;删除后运行 rush update + tsc + build 验证。依赖数量减少后,包的复杂度评分下降,rush install 速度提升也会间接改善 CI 耗时指标。

主要风险在于三类隐式依赖容易被误删:通过 peerDependencies 隐式引用的包、构建工具插件(babel plugin 等在源码里看不到显式 import)、以及 package.jsonbin 字段引用的包。这三类都需要加入白名单,否则删除后运行时会缺失。

生成此 Skill 的 Prompt:

帮我创建一个 dep-cleaner skill,删除 package.json 中未被源码实际引用的冗余依赖。

执行流程:
1. 读取目标 package 的 package.json,收集 dependencies 和 devDependencies
2. 静态扫描 src/ 下所有 .ts / .tsx / .js 文件的 import 语句,提取实际引用的包名
3. 计算差集(声明了但未引用),对每个候选包做二次校验:
   - 排除 peerDependencies
   - 排除构建配置文件中引用的插件(vite.config / rspack.config 等)
   - 排除仅提供类型的 @types/* 包(若对应包存在则保留)
4. 从 package.json 删除确认冗余的依赖
5. 运行 rush update,然后 tsc + build 验证
6. 验证失败则恢复被删依赖,输出无法删除的原因

要求:支持 --yes 参数,全程无需用户确认。

5.3 optimize-any-types

any 类型是 TypeScript 类型检查的"逃生舱"——用一次,那段代码就从类型安全体系里消失了。在大型仓库里,any 会随着业务迭代扩散,直到整个模块都失去类型保护。类型推断链断裂后,IDE 自动补全失效;any 会沿着调用链向上传染,下游函数的类型也随之劣化;重构时无法依赖类型系统做安全检查,回归风险随之上升。

Skill 的做法是用 grep 找出所有显式 any 标注,逐个分析上下文(变量赋值、函数参数、返回值);根据实际使用方式推断精确类型(unknown、具体接口、泛型参数);修改后立即运行 tsc 验证,有报错则针对性修复,直到类型检查通过再继续下一个文件。any 密度下降后,tsc 错误数减少,代码可维护性评分提升

这类任务比前两类稍复杂:将 any 改为具体类型有时会触发下游类型不兼容,引发连锁 tsc 错误,修复范围可能超出预期。第三方库类型缺失时不要强行改,加 // @ts-ignore 并注明原因比引入错误类型更安全;改动量大的文件建议拆分成多次小改,便于 review。

生成此 Skill 的 Prompt:

帮我创建一个 optimize-any-types skill,消除指定 package 中的 TypeScript any 类型。

执行流程:
1. 用 grep 统计目标 package 中所有显式 `any` 的位置和数量
2. 按文件逐个处理,对每处 any 分析使用上下文:
   - 函数参数:根据调用侧传入的实际类型推断
   - 返回值:根据函数体内 return 语句推断
   - 变量声明:根据赋值右侧推断
3. 将 any 替换为精确类型;无法确定时改为 unknown 并添加类型守卫
4. 每处理完一个文件立即运行 tsc,有错误则原地修复后再继续下一个文件
5. 全部处理完后运行完整 tsc 验证
6. 输出:消除 any 数量、剩余无法处理的数量及原因

要求:不引入新的 @ts-ignore;不把 any 改成 unknown 后不加类型守卫就放着不管。

5.4 fix-eslint-disable

// eslint-disable 注释是对 lint 规则的局部豁免,通常是为了绕过一时修不好的问题而临时加上的。加上之后很少有人回头删,逐渐变成永久豁免。积累下去,lint 规则形同虚设,针对这段代码的质量保护完全失效;注释越积越多,新人看到会误以为这是"正确写法",照着复制;真正有问题的代码被 disable 掩盖,隐患持续存在。

Skill 扫描所有 eslint-disable 注释,提取被禁用的规则名;对白名单规则(max-linesmax-params 等纯风格规则)直接跳过;其余规则尝试修复根因,修复后删除注释,运行 ESLint 验证。eslint-disable 密度下降后,代码规范性评分提升

需要特别注意的是两类情况:一是部分 disable 是有意为之(如与第三方库交互时刻意违反某规则),直接删除会引入新问题;二是修复 react-hooks/exhaustive-deps 类规则时要谨慎,补充依赖数组可能改变 effect 触发时机,影响运行时行为——这类改动需要 reviewer 重点关注,不能只看 lint 通过就合入。

生成此 Skill 的 Prompt:

帮我创建一个 eslint-disable-fixer skill,删除不必要的 eslint-disable 注释并修复根因。

执行流程:
1. 扫描目标 package 所有文件中的 eslint-disable / eslint-disable-next-line 注释
2. 提取被禁用的规则名,过滤白名单规则(max-lines、max-params、
   @typescript-eslint/naming-convention 等纯风格规则,直接跳过)
3. 对剩余规则逐个尝试修复:
   - react-hooks/exhaustive-deps:补全依赖数组,但标注"需 reviewer 确认 effect 行为"
   - no-explicit-any:参考 optimize-any-types 的处理方式
   - 其他规则:根据错误信息修复根因
4. 修复完成后删除对应的 disable 注释
5. 运行 eslint 验证,有新报错则回滚该文件的修改
6. 输出:删除注释数、修复规则分布、跳过的白名单规则数

要求:无法修复的注释保留并在 MR 描述中列出,不强行删除。

5.5 fix-ts-expect-error

@ts-expect-error@ts-ignore 更"诚实"——它声明"下一行应该有类型错误",如果下一行实际上没有错误,tsc 反而会报 Unused '@ts-expect-error' directive。但它同样是对类型问题的临时遮盖,而非真正修复。底层类型结构的缺陷被持续掩盖;随着依赖升级,原本的类型错误可能已经消失,但 @ts-expect-error 还在,变成无效注释噪音;类型系统的完整性持续劣化。

Skill 定位所有 @ts-expect-error 注释,移除注释后运行 tsc 观察实际错误:若错误已消失(注释失效)则直接删除;若有真实错误则分类处理,类型不兼容则修改类型声明或添加类型断言;修复后验证 tsc 全量通过。ts-expect-error 密度下降后,类型安全性指标提升,tsc 错误数减少。

这类任务容易遇到的问题是:部分注释压制的是第三方库的类型 bug,库升级后类型可能已修复,但也可能升级后反而变得更严格——移除注释前务必确认实际 tsc 输出。涉及复杂泛型的修复容易引发类型推断链崩塌,建议逐文件处理,避免批量改动引入难以定位的连锁问题。

生成此 Skill 的 Prompt:

帮我创建一个 ts-expect-error-fixer skill,消除 @ts-expect-error 注释并修复底层类型问题。

执行流程:
1. 扫描目标 package 中所有 @ts-expect-error 注释
2. 逐个处理:先移除注释,运行 tsc 观察实际报错
   - 无报错:注释已失效,直接删除
   - 有报错:分析错误类型
     - 类型不兼容:修改类型声明或在调用侧添加类型收窄
     - 缺少属性:补全接口定义
     - 第三方库类型问题:考虑添加 .d.ts 补丁或升级 @types/* 包
3. 每处理完一个注释立即 tsc 验证
4. 全部完成后运行全量 tsc 确认无新增错误
5. 输出:删除注释数(失效 vs 修复)、无法修复的数量及原因

要求:不把 @ts-expect-error 替换成 @ts-ignore;无法修复的保留原注释。

5.6 add-tests

业务迭代快,测试往往是第一个被砍掉的。功能跑通了就上线,测试"以后补"——但以后永远不会来。覆盖率低的包,每次修改都是在裸奔:重构无安全网,回归靠肉眼;依赖这个包的上层代码也失去了集成测试的保护;新人 onboarding 时无法通过测试理解模块的预期行为。

Skill 分析 package 的现有测试覆盖情况,找出覆盖率低的模块;读取源码理解函数的输入输出契约;生成覆盖主路径、边界条件、异常分支的单测;运行 Jest / Vitest 验证全部通过。测试覆盖率指标提升后,包的综合质量评分提升;对代码量无影响,但测试文件数增加。

这是 9 个任务里成功率相对最低的一类,因为它需要理解被测模块的业务语义。但即便成功率只有 50%,每两次执行能产出一个有效的测试用例,而写测试是开发者最不愿意手动做的事情之一。关键风险在于:AI 生成的测试可能断言的是"当前行为"而非"正确行为"——如果源码本身有 bug,测试会把 bug 固化下来。这类 MR 需要 reviewer 对照源码逐用例核对,比其他任务需要更多人工判断。

生成此 Skill 的 Prompt:

帮我创建一个 add-tests skill,为测试覆盖不足的 package 补充单元测试。

执行流程:
1. 检测 package 使用的测试框架(jest / vitest),读取现有 setup 文件和测试配置
2. 运行测试覆盖率命令,找出覆盖率低于 60% 的模块(按行覆盖率排序)
3. 从覆盖率最低的模块开始,逐个生成测试:
   - 读取源码,理解函数签名、参数约束、返回值
   - 生成测试用例:主路径 + 边界条件(空值、越界)+ 异常分支(throw / reject)
   - 外部依赖(网络、文件、时间)一律 mock,不发真实请求
4. 写入测试文件,运行测试验证全部通过
5. 重新生成覆盖率报告,确认指标提升
6. 输出:新增测试文件数、覆盖率变化、跳过的模块及原因

要求:不为 trivial 的 getter/setter 生成测试;测试描述要说清楚"在什么情况下期望什么结果"。

5.7 update-deps

外部依赖不更新,安全漏洞会累积,新版本的 bug 修复和性能改进也享受不到。放着不管,依赖越来越旧,和社区生态脱节;安全扫描告警堆积;某天被迫做一次大版本迁移,成本远高于持续小步升级。

Skill 分析仓库所有外部依赖的当前版本与最新版本,按优先级分层(@types/* 类型包 > 开发工具 > 工具函数库 > UI 组件库);跳过基础设施包(react、typescript、webpack 等);每次选 3–5 个包升级,运行 rush update + tsc + lint + build 验证,失败则回退该包,继续下一个。这类任务的 weight 配置得比较低,因为依赖升级的影响范围更难预判,不适合高频执行。

minor/patch 升级通常安全,但 major 升级要额外谨慎——API 可能有 breaking change;@types/* 包的 major 升级可能引入更严格的类型约束,导致 tsc 新增错误;升级后的包如果有运行时行为变化,质量门禁的编译检查无法发现,需要 reviewer 结合 changelog 判断,这是这类任务最需要人工介入的地方。

生成此 Skill 的 Prompt:

帮我创建一个 update-deps skill,自动升级 monorepo 中的非关键外部依赖。

执行流程:
1. 运行 /analyze-deps 获取所有过期依赖列表及优先级排序
2. 过滤掉以下包(不升级):react、react-dom、typescript、webpack、vite、rollup 及其插件生态
3. 按优先级选取 3-5 个候选包(@types/* > eslint-* / prettier > lodash / dayjs > UI 组件库)
4. 逐包升级:
   a. 修改对应 package.json 的版本号
   b. 运行 rush update
   c. 运行 tsc + lint + build
   d. 通过则继续下一个;失败且无法快速修复则回退版本,记录失败原因
5. 若有任何包成功升级,运行 /create-mr master
6. 输出:成功升级的包和版本变化、失败的包及原因

要求:major 版本升级需在 MR 描述里附上对应包的 changelog 链接。

5.8 upgrade-rush-pnpm

Rushpnpmmonorepo 的基础设施,版本滞后意味着错过性能改进、bug 修复和新特性支持,也可能与新安装的工具包产生兼容性问题。rush install 性能无法享受新版 pnpm 的优化;某些新依赖可能要求更高版本的 pnpmRush 的新功能(如改进的增量构建)无法使用。

Skill 查询 npm 上 @microsoft/rushpnpm 的最新版本;更新 rush.json 中的 rushVersionpnpmVersion;同步更新 .npmrccommon/config 下的相关版本引用;运行 rush update + 全量 build 验证。工具链新鲜度指标提升,间接影响 CI 耗时(新版通常更快)。

这类任务的风险集中在 major 升级上:Rush major 版本升级可能改变 rush.json 的配置格式,需要迁移;pnpm major 升级可能改变 lock 文件格式,导致所有依赖重新解析,CI 缓存失效。升级前 Skill 会确认目标版本的 release notes,但 reviewer 仍需核对是否有已知 breaking change,尤其是涉及 lock 文件格式的变化。

生成此 Skill 的 Prompt:

帮我创建一个 upgrade-rush-pnpm skill,将 Rush 和 pnpm 升级到 npm 最新稳定版本。

执行流程:
1. 查询 npm 获取 @microsoft/rush 和 pnpm 的最新稳定版本号
2. 对比当前版本(读取 rush.json 的 rushVersion / pnpmVersion)
3. 若已是最新则输出"已是最新版本"并退出
4. 更新以下位置的版本引用:
   - rush.json: rushVersion、pnpmVersion
   - common/config/rush/.pnpmfile.cjs(如存在)
   - .npmrc 中的 pnpm 相关配置
5. 运行 rush update(可能需要 --full)
6. 运行 rush build 验证整体构建通过
7. 输出:版本变化(旧→新)、是否为 major 升级(major 升级需在 MR 描述中附 changelog)

要求:major 版本升级时,在 MR 描述里说明主要变更点和潜在影响。

5.9 doc-sync

ARCHITECTURE.mdAGENTS.md 这类上下文文档是给 AI 和新人读的"地图"——但代码在演进,文档不会自动跟着更新。文档和代码的偏差会悄悄积累,直到有人因为看了过时文档而做出错误决策。AI(Claude Code)读到过时的 AGENTS.md 会按错误的规则行事,输出质量下降;新人 onboarding 时被文档误导,踩坑后才发现文档是错的;文档越来越不可信,逐渐变成没人维护也没人看的负担。

Skill 读取现有文档内容,对比实际代码结构(package 列表、目录结构、关键配置);识别过时的描述(引用了已删除的目录、错误的依赖关系、废弃的命令);原地更新对应段落,保持文档风格不变;运行 markdownlint(如有配置)验证格式。文档准确性指标提升,对代码量和测试覆盖无影响。

这类任务是 9 个里最特殊的:它没有编译验证兜底,完全依赖 reviewer 判断准确性。AI 可能把自己不确定的内容也"自信地"更新成错误内容,reviewer 需要对照实际代码逐段核对。正因如此,Skill 被设计成保守策略:只修正明确可验证的错误(路径不存在、包名已改),涉及架构决策和规范约定的段落一律标注"需人工确认"后提 Draft MR,不擅自修改。

生成此 Skill 的 Prompt:

帮我创建一个 doc-sync skill,检测仓库上下文文档与代码实际状态的偏差并更新。

执行流程:
1. 找出仓库中所有 ARCHITECTURE.md、AGENTS.md、CLAUDE.md 文件
2. 对每个文档逐段分析:
   - 提到的目录/文件路径是否仍然存在
   - 提到的 package 名称是否与 rush.json 一致
   - 描述的命令是否仍然有效(在临时目录执行验证)
   - 描述的依赖关系是否与实际 package.json 一致
3. 对明确过时的内容(路径不存在、包名已改)直接更新
4. 对不确定的内容(架构描述、规范说明)标注 TODO 并在 MR 描述里列出,不擅自修改
5. 运行 markdownlint(若配置存在)验证格式
6. 输出:更新的文档数、修改段落数、需要人工确认的条目数

要求:不扩写文档内容,只修正明确错误的描述;保持原有文档风格和语言(中文/英文)不变。

六、最佳实践与踩坑

这套体系跑了几个月,踩过的坑比预想的多。有些是设计失误,有些是对 AI 行为的误判,还有一些是在自动化系统里通用的陷阱。整理出来,希望能帮后来者少走一些弯路。

6.1 明确人与 AI 的边界:AI 执行 + 验证,人只 review

AI 的职责不止于"改代码",还包括跑质量门禁、分析错误、自动修复,直到检测全部通过——这一切都在 MR 创建之前完成。人的职责只有一件事:review MR,决定是否合入。不需要盯着 CI 日志,不需要手动跑检测,不需要介入中间过程。

这个边界一旦模糊,体系就会退化成"AI 帮你写代码,人帮 AI 收尾"的半自动模式,维护成本随之上升,信任也难以建立。

这条原则有一个重要推论:自动化验证的覆盖范围,直接决定了人需要介入的空间大小。质量门禁能自动验证的东西越多,reviewer 需要人工判断的范围就越窄——理想状态是 reviewer 只需要关注"业务语义是否正确",而不是同时还要确认"代码能不能编译"、"类型有没有问题"、"lint 规则有没有违反"。

这意味着搭建 harness 之前,值得先把工程化验证工具配齐。TypeScript 严格模式(strict: true)、ESLint 完整规则集、构建产物的类型校验、单测覆盖率门禁——这些都是可以自动判定通过/失败的验证手段。验证手段越靠前(静态分析 > 编译期 > 运行时),AI 在自愈循环中得到的反馈就越快、越精确,修复方向也更容易收敛。一个只有 build 验证的仓库,harness 的自愈能力会明显弱于一个同时有 tsc strict + ESLint + 单测覆盖率检查的仓库。反过来说,如果发现某类改动总是需要人工介入判断,往往意味着这个维度缺少对应的自动化验证——这是往工具链里补充检测能力的信号,而不是降低对 AI 的要求。

6.2 任务的核心是 Prompt,而不是代码

新增一类维护任务,不需要写任何 TypeScript,只需要在 tasks.md 里追加几行自然语言——描述清楚"先做什么、再做什么、遇到什么情况怎么处理",调度器读到 prompt 就会照着执行。这不是简化,而是一个根本性的设计选择:把任务的执行逻辑从代码层下沉到语言层,意味着任何人都可以直接修改任务行为,不需要理解调度器的实现。

反过来,如果把执行逻辑写进代码里,每次调整都要改代码、走 review、等部署,任务扩展的摩擦力会高出一个数量级。prompt 写得好不好,直接决定任务的执行质量——这是值得认真打磨的地方,而不是能省就省的细节。一个描述含糊的 prompt 会让 AI 在执行时产生偏差,最终体现为莫名其妙的失败或者改错了地方,但问题根源在 prompt 本身,不在 AI。

6.3 任务配置需要持续迭代,失败日志是最好的依据

最初以为把任务注册好、跑起来就完事了。但实际跑一段时间后会发现:某些任务跳过率极高(候选包枯竭)、某些任务频繁在同一个检测步骤失败、某些任务的 prompt 描述不够清晰导致 AI 执行偏差。这些问题不会自动消失,也不该靠直觉猜——每次失败都被记录在运行日志里,这才是真正的调优原料。

不要把失败率高的任务直接下线,先用 harness-audit 分析失败原因:是任务设计问题、是候选包枯竭、还是质量门禁的误判?每一类失败对应不同的修复方向——频繁在 tsc 阶段失败说明 prompt 里缺少类型修复的引导,跳过率高说明候选包筛选条件太严或冷却期太长,同一错误反复出现说明断路器触发了但根因没解决。定期跑 harness-audit,直接修改 tasks.md 权重和 Skill 的 prompt,让系统通过观察自己的失败来改进自己。

6.4 产出速度 > 合入速度,要配套自动 rebase

harness 持续产出 MR,人工 review 和合入的速度远跟不上。积压的 MR 长期不 rebase,会因目标分支持续推进而产生冲突,最终变成需要人工干预才能合入的烫手山芋。配套 rebase-mr skill,同样定期由 CI 触发,自动对所有 open 的 harness MR 执行 rebase。从产出到合入,全程不需要人做任何 git 操作。

6.5 运行日志不能放在共享状态文件里

早期把运行历史记录写入 .claude/harness/history.md。每个 harness MR 都会修改这个文件,导致后续 MR 在 rebase 时必然冲突,几乎每次都需要人工干预。将运行日志拆成独立的 docs/harness/<date>-<task>.md,每次运行生成一个新文件,互不干扰,彻底消除冲突。共享可变状态是自动化系统的大敌,日志文件尤其要注意这一点。

随着运行时间增长还会遇到另一个问题:日志目录里积累了几百个文件,harness-audit 每次需要读取所有文件,速度越来越慢。解决方案是在日志目录中维护一个 index.jsonl,每次运行完成后追加一行结构化摘要(任务 ID、时间、成功/失败、耗时、改动文件数),harness-audit 读取 index.jsonl 做统计分析,只在需要详细信息时才读取具体日志文件。

6.6 逐步扩张任务边界

从最安全的任务开始(dead-code-cleaner、dep-cleaner 这类删除型操作),在团队建立信任后再扩展到更复杂的任务(类型修复、测试补充、依赖升级)。任务粒度(Package 级 vs Repo 级)本身就是风险控制的手段——Package 级任务改动范围小、验证快、失败影响有限,是建立信任的最佳起点。

另一个早期容易踩的坑是把所有任务权重设得一样高,导致高风险任务和低风险任务竞争同等概率。建议按风险分层设置初始权重:dead-code-cleaner 和 dep-cleaner 可以设 10,optimize-any-types 和 fix-eslint-disable 设 6–8,add-tests 和 update-deps 设 3–5,积累足够的成功案例后再逐步调高。


七、想在自己的仓库搭建这套体系?

把以下 prompt 完整发给 Claude Code(claude 命令行工具),它会分析你的仓库结构并引导你完成整套体系的初始化:

我想在当前仓库搭建一套 Harness Engineering 自动化体系,参考以下设计:

## 目标
通过 CI 定时触发,让 AI 全自动完成「选任务 → 改代码 → 质量验证 → 提 MR」的完整链路,
人只需要最终 review MR。

## 需要你帮我完成以下工作:

### 1. 创建任务注册表
在 `.claude/harness/tasks.md` 创建任务注册表,格式如下(每个任务一个 HTML comment 块):

<!-- task
name: <task-name>
description: <一句话说明>
prompt: |
  <自然语言执行步骤,可调用任意 Claude Code skill>
weight: <数字,越大越容易被选中>
enabled: true
-->

先帮我注册以下任务(根据我的仓库实际情况调整 prompt):
- dead-code-cleaner:扫描并删除无引用的死代码文件
- dep-cleaner:删除 package.json 中未被源码引用的冗余依赖
(可根据需要增减)

### 2. 创建调度器 skill
在 `.claude/skills/harness-dispatcher/SKILL.md` 创建调度器,核心流程:
- 读取 tasks.md,按权重加权随机选取一个任务
  (近 7 天执行过的任务权重折半,open MR 多的任务类型降权)
- 基于当前分支创建工作分支(chore/harness-<task>-<YYYYMMDD>)
- 按任务 prompt 执行
- 执行完后运行质量检测(ts-check + lint + build),失败则自动修复,最多循环 5 轮
- 检测通过则提正式 MR,失败则提 Draft MR
- 在 docs/harness/<date>-<task>.md 写入运行日志

### 3. 创建 CI pipeline 配置
创建 CI 定时触发配置(根据我的 CI 系统适配,如 GitHub Actions / GitLab CI),定期触发,执行:
claude -p /harness-dispatcher

### 4. 创建运行历史解析脚本
创建 `.claude/skills/harness-dispatcher/parse-history.js`,读取 `docs/harness/` 下所有日志文件,
返回 JSON 数组,每条记录包含 { task, date, package, success },供任务选取时排除近期已处理的包。

## 注意事项
- 日志文件每次运行生成一个新文件,不要共享单个状态文件(避免 rebase 冲突)
- 质量检测失败时不要静默放弃,要提 Draft MR 留下记录
- 所有步骤全程无需用户确认,完全自动化

请先分析我的仓库结构(package.json、CI 配置、现有 skill 等),然后按上述要求逐步创建文件。

fanwenjie.fe,2026-05-21

On this page