关于 green-gate
面向 Dart 和 Flutter 包的质量门控技能,循环处理分析、格式、测试和覆盖率,直至同一轮观察中全部通过。它通过 MCP 工具执行这些检查,并在进展停滞或决策不明时升级处理。
主要功能
- 01分析、格式、测试与覆盖率门控
- 02每轮重新检查所有门控
- 03自主修复客观失败
- 04依据覆盖率改进测试
- 05遇阻或有歧义时升级
适用场景
- 01发布前检查 Dart 包
- 02处理分析器与测试失败
- 03为未覆盖代码补充测试
- 04验证 Flutter 包质量
MCP 相关
使用 MCP 工具执行 Dart 分析、格式化和覆盖率测试。文档所述的 Claude Code 工作流禁止使用等效 Shell 命令;其他环境在 MCP 服务不可用时可改用命令行。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill green-gate安装后在 Claude Code 中输入/green-gate确认已加载。
附带文件agents/openai.yamlreferences/coverage.md
详细介绍
Green Gate
面向 Dart 和 Flutter 软件包的自主质量门禁循环。运行四道门禁—— 分析、格式化、测试、覆盖率——读取真实工具输出,编辑代码和测试以修复失败, 并持续循环,直到最后一次迭代同时证明四道门禁全部通过,且有观察到的数值。 针对客观失败自主处理;只有遇到停滞、确实存在歧义或基础设施故障时才升级处理。
此技能负责编排工具和编辑文件。测试编写的 方法 由 testing 技能负责——
本技能不会重复介绍模拟、结构或覆盖率模式的指导内容。
核心标准
所有质量门禁工作都应遵循以下标准:
- 只使用 MCP 工具,绝不使用等效的 Bash 命令——通过
mcp__dart__analyze_files进行分析,通过mcp__dart__dart_format进行格式化,通过mcp__very-good-cli__test进行测试和覆盖率检查。每道门禁都有 MCP 工具;没有任何门禁 通过 shell 命令运行。Bash 测试路径(very_good test、flutter test、dart test) 会被block-cli-workarounds.sh钩子拦截并拒绝,而通过 Bash 运行dart analyze/dart format则与 MCP 工具重复。 循环中唯一会运行的 Bash 命令是用于解析coverage/lcov.info的备用方案, 且仅在测试结果不包含Lines not covered:块时才运行。这条仅使用 MCP 的规则是由该 钩子强制执行的 Claude Code 限制;该钩子不会在其他宿主上运行。跨执行环境备用方案: 如果宿主上没有该钩子,也没有连接 MCP 服务,则改为运行等效的dart analyze、dart format和very_good testCLI 命令——绝不要因缺少 MCP 服务而停滞。 - 仅制定计划的请求仍属于此技能的职责——当用户询问要使用哪些
工具、参数或门禁运行顺序,且暂时不希望实际运行时,应依据此技能作答:按门禁顺序
调用相同的工具(使用
applyFixes: true的mcp__dart__analyze_files、mcp__dart__dart_format、使用覆盖率参数的mcp__very-good-cli__test),并说明 决定顺序重要性的优先级规则。绝不要用临时拼凑的 shell 计划(flutter analyze/flutter test --coverage)替代循环实际运行的工具。 - 绝不缓存通过结果——每一轮都重新评估每道门禁。修复分析或测试失败,以及编写新的 测试文件,都会改变格式和覆盖率分母,因此此前通过的门禁可能再次失败。
- 仅在观察到数值后退出——只有在最后一次迭代中,分析结果干净、格式化报告的更改数为
零、所有测试通过且满足
min_coverage,并且这些结果都在同一轮中观察到时,循环才终止。 - 同时传入
coverage: true、min_coverage、check_ignore: true和show_uncovered: true——省略coverage: true会静默地导致不生成lcov.info(类似于 配置错误);省略check_ignore: true会让// coverage:ignore补救措施失效;省略show_uncovered: true会从结果中删去按文件列出的未覆盖行,导致循环转而读取 lcov 文件, 而这些信息原本可以由工具直接返回。show_uncovered从very_good_cli1.5.0 开始提供,这是插件的最低版本要求。 - 将测试编写交由
testing技能负责——当修复需要编写测试时,应遵循该技能对结构、 模拟和命名的指导。 - 修复根本原因,而非表面症状——绝不要通过放宽门禁来通过检查(不要删除失败断言、
降低目标以逃避工作,或对可达代码使用
// coverage:ignore)。遇到确实涉及产品或 API 决策的情况时,应升级处理,而不是自行猜测。 - 针对客观失败自主处理——分析器错误、失败的测试和覆盖率缺口都应直接修复,无需再次询问。 仅在符合矩阵规定的情况下升级处理。
循环
对每个包根目录(参见递归/Monorepo),运行以下算法:
- 发现 — 确定包根目录(
directory参数或工作区根目录);确认其中存在pubspec.yaml。初始化循环状态(迭代计数器0、空指纹集、空的已触及文件集)。 - 分析 — 运行
mcp__dart__analyze_files,并设置applyFixes: true。如果仍有错误,这就是当前未通过的门禁。修复错误,记录指纹,然后转到第 7 步。 - 格式化 — 对包根目录运行
mcp__dart__dart_format。该命令会原地重新格式化整个包。如果它报告有文件变更,则下一轮的此门禁通过。 - 测试 — 仅当本轮分析通过时才执行。使用测试门禁中的覆盖率参数运行
mcp__very-good-cli__test。如果测试失败,进行修复、记录指纹,然后转到第 7 步。 - 覆盖率 — 仅当本轮测试通过时才执行。MCP 的
min_coverage结果是判断通过与否的权威依据,其Lines not covered:块会给出各文件的修复目标;仅当该块不存在时,才解析coverage/lcov.info。如果覆盖率低于目标,则通过testing技能为覆盖不足的文件(按优先级排序)编写测试,然后转到第 7 步。 - 退出 — 如果同一轮迭代中四道门禁全部通过,则用观测到的数据确认成功并停止。这是唯一的通过后退出路径。
- 重新验证 — 递增迭代计数器,重新计算失败指纹,并检查升级触发条件(无进展、振荡、达到上限)。如果触发,则升级处理;否则返回第 2 步,重新评估每一道门禁。
一轮无操作路径 — 适用于原本就已通过的包:第 1 轮迭代发现分析无问题、格式化报告零变更、所有测试通过,且覆盖率达到或超过目标。循环确认全部通过并退出,不编辑任何文件。验证一个通过的包恰好需要一轮迭代。
循环状态
在各轮迭代间保留的状态——没有这些状态,就无法判断是否“没有进展”:
| 状态 | 用途 |
|---|---|
| 迭代计数器 | 强制执行迭代上限(默认每个包 5 轮) |
| 各门禁的失败指纹(上一轮) | 检测无进展和振荡 |
| 本轮触及的文件 | 区分无操作轮次和无进展轮次;填充升级报告 |
各门禁的指纹键:
| 门禁 | 指纹 |
|---|---|
| 分析 | diagnosticCode @ file:line 的排序集合 |
| 格式化 | 格式化门禁重写的文件集合(为空表示通过) |
| 测试 | 失败测试的 ID/名称集合 |
| 覆盖率 | 观测到的百分比 + 覆盖不足的文件路径排序集合 |
定义:
- 无进展 — 当前失败指纹与上一轮相同,或失败数量未减少。升级处理。
- 振荡 — 同两道门禁在连续两轮中交替通过/失败(例如,格式化修复导致分析再次失败,而分析修复又导致格式化再次失败)。升级处理。
Gate 优先级
固定顺序:analyze → format → test → coverage。以下两条规则适用于此顺序:
- 只有本轮上游 Gate 通过后,才会评估下游 Gate。 Analyzer 报错可能导致测试无法编译;测试未通过时,覆盖率没有意义——编译失败后绝不诊断覆盖率。Format 在 analyze 之后运行,这样它的重新格式化就不会与 analyze 的
applyFixes即将重写的代码反复冲突。 - 每一轮都重新评估每个 Gate;通过状态绝不缓存。 只有四个 Gate 在同一轮最终迭代中全部通过,才算完成。
Analyze Gate
- 运行
mcp__dart__analyze_files并设置applyFixes: true,以便在返回诊断结果前应用快速修复。roots接受[{ root: "file:///abs/path" }]——也就是包根目录的file:URI。没有递归标志;该技能会自行枚举包根目录并逐个传入。 - 剩余 error 数量为零时,Gate 通过。将错误级别的诊断视为阻断项;如果警告和信息与修复范围相关,也一并处理,但不要让无关的既存信息阻断 Gate。
- Hook 协作——PostToolUse
analyze.shHook 会在每次 Edit/Write 后触发;如果修复引入了新的 analyze 错误,它会以退出码 2 阻断操作。将此类拒绝视为同一轮的 analyze Gate 反馈,而不是单独的失败类型:编辑并未生效,因此请修改后重试。
Format Gate
- 每一轮都运行
mcp__dart__dart_format。roots接受与analyze_files相同的[{ root: "file:///abs/path" }]格式;省略可选的paths,使其就地格式化整个包。这样 Gate 便基于实际观察结果(能发现手动编辑和既存的格式偏差,而不只是循环编辑过的文件),且能自行修复(一次调用即可让包通过;第二次调用会报告没有更改)。 - 查看更改数量,而非状态——该工具无论是否重写文件都会成功,因此非错误结果并不能证明什么。其输出末尾会显示
Formatted N files (M changed);只有M为0时 Gate 才通过。若某一轮M不为零,文件此时已修复,因此应在下一轮重新运行 Gate,以确认结果为0,而不是在修复后直接判定通过。 - 为什么 format 是真正的 Gate,而不只是 Hook——PostToolUse
format.shHook 只会对循环通过 Edit/Write 编辑的文件触发。循环从未触碰过的手动编辑或既存未格式化文件,否则会通过 Gate,却在 CI 中失败。对整个包执行 format 可以弥补这一缺口。
Test Gate
- 运行
mcp__very-good-cli__test,并设置coverage: true、min_coverage: <target>、check_ignore: true、show_uncovered: true以及exclude_coverageglob(如下所示)。 - Dart 与 Flutter——省略
dart,让工具自动检测(检测到 Flutter 项目时会运行 Flutter)。仅当该工具可能误判纯 Dart 包时,才传入dart: true。 directory——当包不在工作区根目录时(例如 monorepo 子包),请传入该参数;只有pubspec.yaml位于根目录时才省略。timeout_seconds——始终设置上限(例如120)。Flutter 测试在pumpAndSettle()未设置超时时会无限期挂起;上限会终止运行,而不是让循环停滞。因超时而终止属于工具失败,不是测试失败——请按矩阵升级处理。- 所有测试均通过时,Gate 才通过。除非失败的测试体现了真正的产品决策(按矩阵升级处理),否则应自主修复。
覆盖率门槛
MCP min_coverage 结果是通过与否的权威依据;启用
show_uncovered: true 后,结果还会包含各文件的修复目标:
Expected coverage >= 100.00% but actual is 33.33%.
Lines not covered:
- lib/src/weather_repository.dart: 4, 5
根据该区块对文件排序。只有结果中没有此类区块时,才将
coverage/lcov.info 解析作为备用方案:lcov 是某次运行留下的文件,
这次运行可能不是正在诊断的那次;而结果不会过时。
- 默认目标为
100(VGV 的标准),可按调用覆盖 (例如80),用于旧版软件包或非模板软件包。生成文件通过exclude_coverage排除在分母之外,check_ignore: true会遵循// coverage:ignore,因此 100% 表示所有可测试的手写代码均达到 100% 覆盖率。 - 默认
exclude_coverage—**/*.{g,freezed,gen}.dart以及生成文件目录和 l10n 目录。使用单个 glob 字符串(大括号展开;如果 CLI 构建不支持,则 回退到**/*.g.dart)。 - 同时传入
coverage: true、min_coverage、check_ignore: true和show_uncovered: true— 请参阅核心标准。 - 如果结果中没有
Lines not covered:区块,则按照references/coverage.md回退到coverage/lcov.info— 其中涵盖show_uncovered本身、lcov 缺失的三种原因、 过时 lcov 的处理、lcov 记录字段(SF/LF/LH/DA)、 备用方案与门槛保持一致的规则,以及check_ignore仅适用于 Dart 的限制。
修复
- 只修复失败项 — 处理本轮发现的问题、测试失败或覆盖率不足的文件。不要重构无关代码(YAGNI)。
- 覆盖率修复 = 编写测试 — 针对每个排序后的覆盖率不足文件,按照
testing技能编写测试。按未覆盖行数确定文件优先级 — 使用结果中列出的各文件行号,或使用 lcov 备用方案中的LF - LH。 - 限制每轮处理的文件数 — 修复一批相关文件,然后重新验证。每批修复后重新运行门槛检查,才能发现“没有进展”,并避免修复一个门槛时悄然破坏另一个门槛。
升级处理
遇到以下情况时停止并告知用户:
| 触发条件 | 详情 |
|---|---|
| 轮次之间没有进展 | 失败指纹相同或没有减少 |
| 来回振荡 | 同两个门禁在两轮中交替变为绿灯/红灯 |
| 已达上限,门禁仍为红灯 | 终止 — 按门禁报告剩余失败及其指纹 |
| 红灯测试确属产品缺陷 | 看起来源于真实产品决策而非编码错误的失败 |
| 修复方式不明确 | 存在多种有效解决方案(例如修改 API 或抑制 lint)——优先修复根本原因;若涉及产品/API 决策,则升级处理 |
| 不可达代码的覆盖率缺口 | 建议使用 // coverage:ignore(需要设置 check_ignore: true,仅限 Dart),而不是追求 100% |
| 分母规范 | 生成文件未被排除 glob 匹配——扩大 exclude_coverage 的范围,而不是使用 // coverage:ignore |
| 工具/钩子失败 | MCP 测试超时(timeout_seconds 触发终止)、分析器崩溃、因缺少 CLI 而被钩子拒绝(升级处理时附上安装提示 dart pub global activate very_good_cli),或修订后 analyze.sh _重复_拒绝,仍阻碍所需修改——单次拒绝属于循环内的分析门禁反馈(见 分析门禁),不应升级处理 |
| 升级问题时,请指出变红的门禁——analyze、format、test 或 coverage—— | |
然后逐字给出其指纹条目(analyze 的格式为 diagnosticCode @ file:line,test | |
| 为失败的测试名称,coverage 为覆盖率不足的文件路径)、已修改的文件、迭代次数,以及用户必须做出的唯一决定。总结数量(“还剩 4 个错误”)不算报告;用户无法据此采取行动。 |
要求持续尝试的指令不能覆盖触发条件。“只要需要就继续重试,别回来找我”并不意味着可以把预算花在指纹已停止变化的情况上。达到触发条件时就停止、报告并等待。
递归 / 单体仓库
- 所有包都必须通过——失败时继续处理(修复每个失败的包),然后确认每个包的结果。一个包的门禁变红不会中止其他包的检查。
- 按包计算迭代预算——上限 5 次适用于每个包,而非全局。因此,包含 12 个包的单体仓库不会在第一个包上耗尽全局预算。
- 共享包根目录发现机制——遍历查找
pubspec.yaml文件。analyze_files的roots集合必须与mcp__very-good-cli__test --recursive(recursive: true)所覆盖的包集合一致。 - lcov 备用路径为
<package>/coverage/lcov.info——根据发现的根目录逐一解析。 - 单个
min_coverage适用于所有包——已说明的限制:工具架构不支持按包覆盖率设置不同目标。确认覆盖率时,请说明这个共享目标。
其他资源
references/coverage.md— green-gate 的覆盖率门禁详情(默认目标、排除 glob、show_uncovered、lcov 备用路径及其字段、决策树、check_ignore、过时的 lcov)。testingskill — 测试约定,以及用于弥补逐文件覆盖率缺口的覆盖率参考。