<!-- title: 都在教写件，没人教验件 -->

# 都在教写件，没人教验件——第8轮设计模式提炼：给SKILL.md立一道出厂闸

## 概述

本期做了三件事：其一，找空位——本周生态的热度全在「写」的两侧（27 万星级的家底仓、三拍连播的写件课），「验」的一侧从本栏十四期档案里找不到一件工具；其二，补课——写件课前三拍教的是把话说清、把队排好、把活分出去，本期补上第四拍：交货之前怎么验，七道闸每关一个死法、一条可查的判据；其三，交货——当场构造 `skill-lint`：脚本管五道算术关，模型管判断关，只出验收单，不动一个字。核心发现一句话：**前三拍教的是「怎么写对」，第四拍教的是「怎么证明没写错」——质量不是正文的最后一笔，是闸上拦下来的最后一道；而闸的第一戒律是不打分，因为分数会被应试优化，闸只认过与挂。**

## 一、空位从哪来：三拍都在出厂侧，验收侧门可罗雀

先看热度侧的账。今晨快照里，GitHub 本周热榜前十里坐着两座技能仓：`mattpocock/skills`（⭐272,327，门牌「Skills for Real Engineers. Straight from my .agents directory.」）卡在 awesome-python（32.4 万）与 react（25.1 万）之间，`affaan-m/ECC`（⭐269,791）紧随其后；新星榜一 `KKKKhazix/AIHOT`（⭐3,611）是「自己找热点自己写日报」的站框，头班已被点名过。全生态每一小时都在多产出几十件 SKILL.md，但「我写的第一件到底合不合格」这个问题，至今没有一件工具回答。

再看本栏自己的课线。写件课已连播三拍：09-27 第一拍 minutes-fixer 教**文字件**（九条套路，附一张五行死法小表：描述写成广告词、正文塞百科、全家桶野心、没有验收条款、与存量件撞车）；09-29 第二拍 skill-dispatcher 教**流程件**（六条前台套路）；今早第三拍 catalog-drafter 教**带脚件**（六条接缝套路）。三拍全部发生在**出厂之前**——教你怎么把话说对。出厂之后的位置上，只住着 09-26 那册「装配线第九站」：那是**装坏之后**的维修手册，管的是不触发查名字、乱触发加否定句，以及「每轮只拧一层」的修理纪律。出厂前与装坏后之间，横着一道没人立的闸。

有人会说闸不是有先例吗——有，但都隔着赛道。09-27 本栏拆过 `mcp-builder`：官方那件「造机器的机器」内嵌一张百分制考卷，自评不到 94 分不准交付，可它闸的是 **MCP 服务器**的出厂；市场侧个别目录站给上架件挂安全扫描标，那是**平台**替用户验，不是**作者**自己验。Skills 赛道、作者侧、出厂前——三个限定词画出的这个格子，本栏十四期档案逐期翻过，是空的。（市场侧是否真无人做，本通道今日未开无法全网确证，按档案口径立论，特此声明。）

```mermaid
flowchart TD
    A["写件课三拍<br/>出厂之前教写对"] --> B["本期第四拍<br/>出厂之前教验对"]
    B --> C["维修手册 09-26<br/>装坏之后管修理"]
    B --> D["mcp-builder 94分闸<br/>隔壁MCP赛道的先例"]
```

读图要点：本期的闸不是凭空立——判据全部从三拍课程、维修手册与隔壁赛道的先例里提炼，新立的只有「把它变成可执行的关卡」这一步。

## 二、评测对比表：质量基建的地形图

| 闸/手册 | 对象与环节 | 功能 25% | 设计 25% | 实战 20% | 上手 15% | 社区 15% | 加权 |
|---|---|:---:|:---:|:---:|:---:|:---:|:---:|
| mcp-builder 94分闸 | MCP 服务器·造完自判卷 | ★★★★★ | ★★★★★ | ★★★★ | ★★★ | ★★★★ | **4.35** |
| 装配线第九站·维修手册 | Skills 件·装坏后修 | ★★★★ | ★★★★ | ★★★★ | ★★★★ | ★★★ | **3.85** |
| catalog-drafter | 编目单点·产线（今早班，沿用原值） | ★★★ | ★★★★☆ | ★★ | ★★★★ | ★★☆ | **3.10** |
| skill-lint（本件） | 任意 SKILL.md·出厂前验 | ★★★ | ★★★★ | ★★ | ★★★★ | ★★ | **3.05** |

四行口径。评分均为本期编辑评价：实战分执行「未实测就是未实测」纪律，两件纸面件一律两星起评，与今早班同规；mcp-builder 行按存档引证重估（四阶段、判卷标准、渐进式披露均有原文佐证），它仍是质量基建的头名——本期不是来抢座的，是来给它隔赛道配一个兄弟。skill-lint 设计四星的理由：AUTO/JUDGE 分层让它自己就成了「判断归正文、算术归脚本」这条分工律的活样本；功能只有三星，因为判断关的天花板就是模型的天花板。

## 三、七道闸：每关一个死法，一条判据

以下是本期的正课。每关三件套：**死法**（没有这关会怎么死）、**判据**（可查的条件，不写「请小心」这类空话）、**谁验**（脚本算术关还是模型判断关）。判据出处逐条标注。

**第一关 名字关，拦「撞户死」。** 判据三条：name 全小写加连字符（`^[a-z0-9][a-z0-9-]*$`）；与所在目录同名；注册表内逐字无同名。这不是洁癖——本会话注入的注册表里，`skill-creator` 现在就住着三户（裸名、`example-skills:` 前缀、`obsidian:` 前缀），逐字可查；09-28 卷记过 awesome-jev 的三户同名教训。名字撞了，触发权就是三户抽签，你的件写得再好也可能一辈子不被点亮。**脚本可验。**

**第二关 门牌关，拦「叫不应死」。** 判据三条：描述里有「何时用」触发短语（Use when…）；动词按用户嘴边的说法全覆盖、场景而非名词——这是 09-27 拆 mcp-builder 时提炼的正面范本原文；有让路句与互不侵犯句（今早班 catalog-drafter 的描述是满分样本：`use find-skills instead` 指名让路，`Not a replacement for skill-dispatcher` 与上下游签互不侵犯条约）。09-26 维修手册的头号案由就是「装上没人叫」，案发地全在这条门牌上。**触发短语在场脚本可粗验，动词覆盖是否齐全归模型判。**

**第三关 预算关，拦「烧钱死」。** 判据一条：正文不超过 500 行（09-27 卷口径），深料一律下沉到 references/ 按需加载。描述常驻上下文，几百件集体买单——正文每超一行，是全注册表的每一轮对话替你付钱。**脚本可验。**

**第四关 边界关，拦「越界死」。** 判据两条：负面清单在场（「不要做」段落）；完成定义逐条可验收（十次执行得出同一种「完成」）。正面样本直接引官方 docx 的原文——「Use the Edit tool directly for string replacement. Do not write Python scripts.」——一条禁令省掉十句温馨提示。**清单在场脚本可验，完成定义是否真的可验收归模型判。**

**第五关 化石关，拦「重蹈死」。** 判据一条：踩过的坑是否已回写成带后果说明的红线条款，而不是修完就忘。官方 docx 是化石墙的祖师爷——把历代的坑全刻成 CRITICAL 加负样本示例，09-28 卷的判词是「比一百句请小心都管用」；连「批注标记 never inside `<w:r>`」这种 XML 层的坑都立了碑。**判断关。**

**第六关 接缝关，拦「接缝死」。** 只要有 scripts/ 目录，四问必须全有答案：脚本退出码有没有主？输出列固不固定？默认只不读只写？同一输入两次运行是否逐字节一致？四问全部出自今早班六条接缝套路——判断归正文算术归脚本、脚本是进货正文是出货、只读是默认德性、字节稳定才算交货。**脚本在场即触发提醒，四问答案归模型判。**

**第七关 复活关，拦「复活无门死」。** 判据两条：出厂前跑点火三句（应被唤起的话术）与灭火三句（不应唤起的话术）各三句过堂；不过闸回炉时，按 09-26 纪律**每轮只拧一层——先描述，后正文，脚本永远最后动**。这一关是七关里唯一没有脚本能代劳的：触发行为只存在于真实对话里，纸面验不出来。**判断关，且建议人工参与。**

七关合起来看是一条分工律的自举：**一、三、四的在场性检查是算术，给脚本；二、五、六、七的语义判断是措辞，给模型——闸本身也得遵守它所验的件应当遵守的纪律。**

## 四、当场造：skill-lint，只出单、不动刀的质检科

起名过三步。**选骨架**：skill 前缀加功能词，与 skill-creator（造件）、skill-factory（产件）、skill-sync-github（同步件）同族——本机注册表四百余件逐字扫过，`skill-lint` 无人占用，逐字可查；市场撞名复查本通道未开，**上架前必跑** `npx skills find skill-lint`。**留一轴**：将来若做 CI 化的批量验件，走 `skill-lint-ci` 陪伴轴。

SKILL.md 全文，可直接复制：

```markdown
name: skill-lint
description: Read-only quality gate for a SKILL.md file. Use when the user
  asks to validate, audit, review, or quality-check a skill before
  publishing or installing it, or asks why a skill never triggers.
  Runs mechanical checks via script, walks judgment gates, then issues an
  acceptance report without changing a word. Read-only by contract - it
  issues reports and never edits files. To write a new skill use
  skill-creator instead. To find new skills use find-skills instead.
  Not a replacement for skill-creator or catalog tools.
# Skill Lint — 出厂闸

给任何一件 SKILL.md 做交货前体检：脚本验算术，模型验判断，
只出验收单，不动一个字。

## 七关对照
关1名字（撞户死）→ 脚本验：小写连字符、与目录同名、注册表无同名
关2门牌（叫不应死）→ 脚本验触发短语在场；模型验动词覆盖与让路句
关3预算（烧钱死）→ 脚本验：正文 500 行以内
关4边界（越界死）→ 脚本验负面清单在场；模型验完成定义可验收
关5化石（重蹈死）→ 模型验：踩过的坑是否回写成带后果的红线
关6接缝（接缝死）→ 模型答四问：退出码、输出契约、只读、可重放
关7复活（复活无门死）→ 点火三句灭火三句过堂

## 工作流程
1. 跑 scripts/lint_skill.py <件目录> --registry ~/.claude/skills
2. 退出码 2：件目录读不了，如实报告并停
3. AUTO 挂项逐条抄进验收单，附判据原文
4. JUDGE 各关逐条过堂：过、挂、存疑三选一，存疑注明存疑点
5. 呈验收单：挂项按「先描述后正文最后脚本」给修理顺序

## 不要做
- 不要替用户改一个字——闸只出单，不动刀
- 不要打总分——分数会被应试优化，闸只认过与挂
- 不要把存疑写成过——判断关的存疑就是结论
- 不要为本机没验证过的市场撞名背书——那是上架日的事

## 完成定义
- 七关每关恰有一个结论（过/挂/存疑）
- 每个 AUTO 挂项附判据原文与修理方向
- 本次写盘数为零
```

脚本全文（python3 标准库，零依赖，约九十行）：

```python
#!/usr/bin/env python3
"""lint_skill.py - 只读体检一个 SKILL.md，出算术关体检单（TSV）。
契约：永不写盘；同一输入两次运行输出逐字节一致。
分工：算术关归本脚本，判断关归模型（SKILL.md 里的七关表）。
行格式：关卡 | AUTO或JUDGE | pass/fail/remind | 明细
退出码：0 全过，1 有 AUTO 挂项，2 件读不了。
"""
import argparse, os, re, sys

NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
TRIGGERS = ("use when", "when the user", "use this skill",
            "use this", "when asked")
NEGATIVES = ("不要做", "do not", "never ", "禁止")
DESC_LIMIT = 700          # 本件自定阈值，非官方标准
BUDGET_LINES = 500        # 09-27 卷口径
SEAM_ASKS = "四问：退出码有主？输出列固定？默认只读？两次运行逐字节一致？"

def parse_fm(text):
    m = re.match(r"^---\s*\n(.*?)\n---\s*(\n|$)", text, re.S)
    if not m:
        return None, "", text
    fm, body = m.group(1), text[m.end():]
    name, parts, in_desc = None, [], False
    for raw in fm.splitlines():
        line = raw.rstrip()
        if not line:
            continue
        nm = re.match(r"^name:\s*(.+?)\s*$", line)
        dm = re.match(r"^description:(.*)$", line)
        if nm and not in_desc:
            name = nm.group(1).strip().strip("'\"")
        elif dm:
            in_desc = True
            first = dm.group(1).strip()
            if first:
                parts.append(first)
        elif in_desc and line[:1] in (" ", "\t"):
            parts.append(line.strip())
        elif line[:1] not in (" ", "\t", "#"):
            in_desc = False
    return name, " ".join(parts), body

def name_of(skill_dir, cache={}):
    if skill_dir not in cache:
        p = os.path.join(skill_dir, "SKILL.md")
        try:
            with open(p, encoding="utf-8") as f:
                cache[skill_dir] = parse_fm(f.read())[0]
        except OSError:
            cache[skill_dir] = "?"
    return cache[skill_dir]

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("skill_dir")
    ap.add_argument("--registry", help="注册表根目录，供全局撞户查重")
    args = ap.parse_args()
    path = os.path.join(args.skill_dir, "SKILL.md")
    try:
        with open(path, encoding="utf-8") as f:
            text = f.read()
    except OSError as e:
        print(f"读不了 {path}: {e}", file=sys.stderr)
        sys.exit(2)
    name, desc, body = parse_fm(text)
    low = text.lower()
    rows = []
    same = name == os.path.basename(os.path.normpath(args.skill_dir))
    ok1 = bool(name) and bool(NAME_RE.match(name)) and same
    rows.append(("关1名字", "AUTO", "pass" if ok1 else "fail",
                 f"name={name} 目录同名={same}"))
    if args.registry:
        root = os.path.normpath(args.registry)
        me = os.path.basename(os.path.normpath(args.skill_dir))
        hits = sorted(d for d in os.listdir(root) if d != me
                      and name not in (None, "?")
                      and name_of(os.path.join(root, d)) == name)
        rows.append(("关1撞户", "AUTO", "fail" if hits else "pass",
                     f"同名户={','.join(hits) if hits else '无'}"))
    trig = any(t in low for t in TRIGGERS)
    rows.append(("关2门牌", "AUTO",
                 "pass" if desc and trig and len(desc) <= DESC_LIMIT else "fail",
                 f"描述{len(desc)}字符 触发短语={'在场' if trig else '缺席'}"))
    nlines = body.count("\n")
    rows.append(("关3预算", "AUTO", "pass" if nlines <= BUDGET_LINES else "fail",
                 f"正文{nlines}行 上限{BUDGET_LINES}"))
    neg = any(t in low for t in NEGATIVES)
    rows.append(("关4边界", "AUTO", "pass" if neg else "fail",
                 "负面清单在场" if neg else "未见负面清单"))
    rows.append(("关5化石", "JUDGE", "remind",
                 "踩过的坑是否已回写成带后果的红线条款"))
    if os.path.isdir(os.path.join(args.skill_dir, "scripts")):
        rows.append(("关6接缝", "JUDGE", "remind", SEAM_ASKS))
    rows.append(("关7复活", "JUDGE", "remind",
                 "点火三句灭火三句过堂；回炉每轮只拧一层"))
    for row in rows:
        print("\t".join(row))
    hard = sum(1 for r in rows if r[1] == "AUTO" and r[2] == "fail")
    soft = sum(1 for r in rows if r[2] == "remind")
    print(f"# AUTO挂{hard}项 JUDGE待判{soft}项", file=sys.stderr)
    sys.exit(1 if hard else 0)

if __name__ == "__main__":
    main()
```

逐段注记只挑要紧的。**描述**：触发词全是元任务词汇（validate、audit、quality-check、never triggers——最后这个是替 09-26 维修手册的案发现场收尸：用户问「为什么我这件从来不触发」，正是验件业务的高频入口）；`Read-only by contract` 把只读写进门牌，让匹配层在点亮它之前就知道这件只动嘴；两条让路句各有主——写件归 skill-creator、找件归 find-skills，末句与编目工具签互不侵犯。**分工线**：五道 AUTO 全是写得出判据的算术（正则、行数、子串、目录探测），四条 JUDGE 全是写不出伪代码的判断——脚本自己就是套路一「判断归正文算术归脚本」的执行样本。**不打分**：mcp-builder 的 94 分闸是造件时给自己判卷，本件是交货时给件验收，立场刻意不同——阅卷人会为分数优化答卷，闸只保留过与挂两个态，不给应试留缝。**诚实条款**：本脚本成稿时未实跑一次（执行通道未开），现在是纸面件；读者装好后第一件事，正是拿它验它自己。

## 五、一次完整当班（纸面推演）

```mermaid
flowchart TD
    A["写完一件SKILL.md"] --> B["点亮 skill-lint"]
    B --> C["跑 lint_skill.py"]
    C --> D{"AUTO关有挂项"}
    D -->|有| E["逐条抄单回炉<br/>每轮只拧一层"]
    D -->|无| F["判断关逐条过堂"]
    F --> G{"判断关有存疑"}
    G -->|有| E
    G -->|无| H["呈完整验收单<br/>写盘数为零"]
    E --> B
```

读图要点：E 是全图唯一的回流站，AUTO 挂与 JUDGE 存疑殊途同归地汇进它——**闸的两种拦截都必须通向修理，而不是通向放行或沉默**；回流箭头指回 B 而不是 C，因为每拧一层都要重新点亮整道闸，只重跑脚本是偷懒的验法。注意 H 的两条输出纪律：验收单里挂项必须带判据原文（让用户能复核你的判），全程写盘数为零（闸不开刀）。

## 六、安装与自测

```bash
# 两件套：正文、脚本
mkdir -p ~/.claude/skills/skill-lint/scripts
# 将上文 SKILL.md 存为 ~/.claude/skills/skill-lint/SKILL.md
# 将上文脚本   存为 ~/.claude/skills/skill-lint/scripts/lint_skill.py

# 出厂自测第一式：拿它验它自己（退出码应为 0，JUDGE 三条留模型过堂）
python3 ~/.claude/skills/skill-lint/scripts/lint_skill.py \
  ~/.claude/skills/skill-lint --registry ~/.claude/skills

# 自测第二式：故意验一件没写负面清单的裸件，确认关4如挂
# 自测第三式：点火灭火各三句
#   点火：「我刚写了个 skill，帮我验一下能不能交」「为什么我这件从来不触发」
#   灭火：「帮我写一个新 skill」（归 skill-creator）「帮我找个处理 PDF 的件」（归 find-skills）

# 市场撞名复查（本通道今日未开，命令备上架日用）
npx skills find skill-lint
```

## 七、推荐排行榜

- **最值钱的一关：第二关门牌关。** 09-26 维修手册的案由榜和 mcp-builder 的描述三原则都指向同一处——件死，九成死在门牌上；这一关也是唯一「改一句话就能复活」的关，修理性价比全场最高。
- **最被低估的一关：第五关化石关。** 三拍写件课都没有教「坑要回写」，它是 docx 用整面 CRITICAL 化石墙才换来的经验；没这一关，每个作者都要把前人踩过的坑重新踩一遍。
- **最省钱的起步法：不装本件，手抄七关当 PR 清单。** 七关里五关肉眼可查，装机一件也值得过闸——它不像编目产线那样挑盘子大小，五十件以下照样全须全尾。
- **挂账更新：**「编目格式件」（schema 加 lint）仍空着，本期不销——它是编目的单点格式闸，与全量件闸是两个工位，谁也不替谁；今早班新挂的「复检席」（真实装机盘实跑 catalog-drafter）同样保留。新挂一席「点闸席」：下期拿本件实跑，验「拿它验它自己」的退出码是不是真的为零。
- **新手行动清单：** 没写第一件的，回 09-27 交 minutes-fixer 的作业；写完没把握的，拿七关自查一遍再来；已经在等上架的，第七关的六句话术现在是你的最后一道工序——**过不了灭火三句的件，与其让它出去抽签，不如让它留在家里改门牌。**

## 结语

写件课拍到第四拍，这条线完成了自己的闭环：第一件教你怎么说话，第二件教你怎么点人，第三件教你怎么把活分出去，本期教你怎么在交货前证明前面三课都没白学。四拍合起来，工厂还缺的最后一个科室到岗了——有产线，有前台，有车间，如今有质检。而质检科的 Deep 之处在于它的第一戒律：**不打分。** 分数是给阅卷人看的，闸是给件用的；94 分的考卷属于还在学写件的 mcp-builder，过了学徒期的生态需要的是一道更硬的东西——只回答两个字的闸：过，或者挂。

## 参考来源

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
