LeMetaSkill v1.1.0:跨 runtime(ZCode/Claude Code/Codex)的 Agent Skill 创建与迭代工坊

- SKILL.md 双工作流(创建/迭代)+ 两道门(验料/通用化)+ 三档位
- 7 篇方法论 references + 3 个纯标准库脚本(scaffold/validate/trigger_eval)
- 自身触发用例 16 条 + 先例研究/创建/迭代证据报告
- 方法论吸收自 qiaomu-meta-skill 与 luban-skill,取舍见 reports/prior-art.md
This commit is contained in:
Leroy
2026-10-05 12:09:22 +08:00
commit d3e9bd45bd
21 changed files with 1827 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
---
name: le-meta-skill
description: |
LeMetaSkill —— Leroy 的 Skill 工坊:创建与迭代 Agent Skill,ZCode / Claude Code / Codex 等 runtime 通用。把重复工作流、提示词、SOP、脚本、对话中反复出现的方法,变成可触发、可验证、可维护的 Skill;也负责改进、重构、体检、触发测试已有 Skill。
何时触发:用户想新建/创建/起草 skill("帮我做个 skill""把这个流程沉淀成 skill""做成可复用的技能""turn this into a skill");想改进/优化/升级/重构/体检已有 skill("这个 skill 触发不灵""给我的 skill 做个体检""改进这个 skill");想为 skill 设计触发测试或结构检查;丢来一个 SKILL.md、skill 目录或想法说"帮我看看怎么弄好",只要上下文是想让Skill变得可复用,都应触发。
不要触发:与 skill 无关的普通提示词改写、一次性任务、代码 review、MCP/插件/斜杠命令配置排障。
metadata:
author: Leroy
version: "1.1.0"
runtimes: zcode, claude-code, codex
inspiration: joeseesun/qiaomu-meta-skill; LearnPrompt/luban-skill; ZCode skill-creator
---
# LeMetaSkill —— Leroy 的 Skill 工坊
把「重复出现的方法」变成「可触发、可验证、可维护的 Skill」。
判断放 `references/`,确定性检查放 `scripts/`,触发用例放 `evals/`,运行证据放 `reports/`。本文件只保留路由和最简工作流。
## 工坊总则
- **单一创建权威。** 本技能触发后,由它负责从意图、先例、蓝图、起草、验证到交付的完整流程;不要再同时调用宿主内置的 `skill-creator` 或其它 meta 技能——双重权威会导致冲突规范和重复提问。只有用户明确要求横向对比创建器、或本技能不可用时才引入其它工具,并说明职责边界。
- **写准,不写长。** 目标是 description 精准触发、正文精炼可执行。文档更长不等于技能更好;每一条规则都要为稳定性挣到自己的位置。
- **按档位比例投入。** 复用范围决定档位(见下)。只创建"挣来的"文件——每个文件必须回答它提高了哪方面的稳定性(判断力/确定性/可回归/证据),不造仪式性目录。
- **证据分级。** 交付时每个亮点标注 `[设计优势]`(源码可见的设计选择)、`[已验证]`(有触发/输出/实测证据)、`[假设]`(合理但未证明)。验证不了的写「证据缺失」,不写漂亮话补位。
- **中文优先,标识符英文。** 面向 Leroy 的说明、报告默认中文;`name`、目录、代码用英文 kebab-case。
- **先验证触发,再扩展内容。** description 是唯一的自动触发信号,先让它在真实对话里可靠触发,再扩正文——顺序反了会把房子盖在流沙上。
## 第一道门:验料——值不值得做成 Skill
动手前先回答(详见 [意图工坊](references/intent-workshop.md)):
1. **重复性**:这类任务会反复出现吗?只出现一次的,做成 skill 是浪费。
2. **稳定性**:流程和规则相对稳定吗?需要大量实时判断、没有固定流程的,保留为普通对话步骤更好。
3. **增益**:做成 skill 比每次现场对话强在哪——一致性、触发可靠、携带脚本/模板、跨会话沉淀?说不出来就先不做。
```text
验料结论:[好料 / 料可用但需调整定位 / 朽木——建议保留为对话步骤或先积累案例]
```
判定为朽木时**停手直说**,给出替代路径,不要硬做。
## 第二道门:通用化门
把单次失败或单例反馈升级为核心规则之前:
1. 先改写成领域中立的行为描述(不绑定那个具体项目/文件/那次失误);
2. 分类:核心机制、可选适配、还是仅测试用例;
3. 只有安全、事实、权限类硬边界,或**在两个不相关领域都复现过**的行为,才进 SKILL.md 正文;
4. 一次性细节放进 fixtures、示例或专项 reference。
这条门防的是过拟合:为一次翻车写的规矩,会变成以后每次的摩擦。
## 档位
| 档位 | 适用 | 必备件 | 验收门 |
|---|---|---|---|
| 轻量 draft | 个人试验、自用 | `SKILL.md` | frontmatter 合法、description 有触发词和负触发 |
| 共享 shared | 团队/跨项目复用 | + `evals/trigger_cases.json`、README 钩子 | 结构体检全绿、触发冒烟测试通过 |
| 严谨 rigorous | 长期维护的基础能力、meta skill | + `reports/` 证据、参考文档分层 | 共享档全部要求 + 交付报告带证据分级 + 真实对话实测记录 |
升级时机:别人开始复用 → draft 升 shared;成为基础设施或多项目依赖 → shared 升 rigorous。默认选最轻的够用档位。
## 创建工作流(CREATE)
1. **接单**:按 [意图工坊](references/intent-workshop.md) 捕获七要素(重复任务/输入/交付物/排除项/标准/参考/已有资产)。信息足够就直接做并写出假设,不做问卷。
2. **验料**:过第一道门。不过,停手给替代。
3. **先例研究**:按 [先例研究](references/prior-art.md) 搜 2–4 个同类,用 `keep/adapt/reject/invent` 提炼机制——只借结构和方法,不复制表达。轻量档可跳过,但要说明为什么。
4. **蓝图**:定档位和目录结构(见 [目录架构](references/architecture.md)),**先写 description** 再动正文。
5. **起草**:`python3 scripts/scaffold.py <name> "<一句话职责>" --tier <draft|shared|rigorous>` 生成骨架,按 [写作手艺](references/writing-craft.md) 填充。
6. **触发验证**:`python3 scripts/trigger_eval.py <skill目录>` 跑冒烟测试,再在真实对话里测 2–3 条用户原话(方法见 [触发测试](references/trigger-eval.md))。触发不过,回头改 description,不扩内容。
7. **交付**:按 [交付报告](references/handoff.md) 输出 handoff,亮点标证据等级。
## 迭代工作流(ITERATE)
改进已有 skill 时(详见 [迭代工坊](references/iteration.md)):
1. **冻结基线**:备份原版(`cp -r` 或 git 分支),所有改动与基线比。
2. **诊断三把尺**:结构尺(`python3 scripts/validate_skill.py <目录>`)+触发尺(trigger_eval + 真实对话)+活体尺(拿真实任务实际跑一遍,"看起来能触发"不等于"跑得出结果")。
3. **评分定位**:六维评分(触发与边界/工作流清晰度/可执行具体性/失败模式与停手点/实测表现/资源纪律),每个分数给证据,找出最该刨的一个面。
4. **单面慢刨**:一轮只改一个面;批量授权后也是单面单提交,绝不混刨。
5. **验证门**:改动要全部满足——比基线多过至少 2 条测试原话、没把 skill 写得更长但更难用、没引入私有路径或不可复现依赖——才保留,否则回刀。
6. **交付**:改动报告 + 回炉清单(下一轮入口、本轮不做)。
## 核心命令
```bash
python3 scripts/scaffold.py <name> "<一句话职责>" --tier shared # 起草骨架
python3 scripts/validate_skill.py <skill目录> # 结构体检
python3 scripts/trigger_eval.py <skill目录> # 触发冒烟测试
```
脚本无第三方依赖,python3 标准库即可运行。
## 交付契约
只交付所选档位"挣来"的东西:工作中的 skill 目录、通过的体检/触发证据、handoff 报告。不发布(无发布流水线)、不虚构验证结果、不交付用例覆盖不了的承诺。
## 强制停手点
以下节点必须停下等用户确认:
1. 验料判定"朽木"时——给出替代方向后停,不硬做;
2. 改动会大幅改变 skill 的定位或职责边界时;
3. 需要删除或重写大段现有内容时;
4. 新 skill 会与已安装的其它 skill 同名(同名覆盖会改变触发优先级)时;
5. 迭代中要引入高风险脚本、删除逻辑或外部 API 调用时。
## 反例黑名单
- 不要把一次性总结、翻译、解释、头脑风暴做成 skill。
- 不要只改 SKILL.md 正文而不验证触发——description 才是触发信号。
- 不要为了让 skill 显得专业堆术语、堆 MUST/NEVER;讲清楚为什么,比喊口号有效。
- 不要把私有路径、密钥、私人账号写进任何文件。
- 不要在没有两个领域复现证据时,把单例教训升为核心规则。
- 不要一轮迭代刨多个面;拿到批量授权后也不要把多个面塞进一次改动。
- 不要让同一只手既"改"又"评"——验收时换成第一次见到这个 skill 的陌生用户视角。
- 不要凭记忆编造先例;每个参考都带真实 URL,搜不到就如实标注。
## Reference Map
- 接单与验料:[意图工坊](references/intent-workshop.md)
- 搜同类:[先例研究](references/prior-art.md)
- 写 description 与正文:[写作手艺](references/writing-craft.md)
- 目录怎么摆:[目录架构](references/architecture.md)
- 触发怎么测:[触发测试](references/trigger-eval.md)
- 改进已有 skill:[迭代工坊](references/iteration.md)
- 交付怎么说:[交付报告](references/handoff.md)
@@ -0,0 +1,31 @@
---
name: {{name}}
description: |
{{tagline}}
【TODO:做什么 + 何时触发,2-3 句。给出用户真实会说的原话 3-8 条:"帮我……""把这个……"】
不要用于:【TODO:负触发,至少 2 条相邻请求】。
---
# {{name}}
一句话定位:{{tagline}}
## 何时使用
【TODO:触发场景 2-3 个;与 description 呼应但不重复堆词】
## 工作流
【TODO:分步流程。每步用祈使句,给输出格式或示例;超过 300 行考虑拆 references/】
```text
示例:输入 <X> → 步骤 → 输出 <Y>
```
## 失败模式与停手点
【TODO:遇到什么情况停下来问用户;出错时的 fallback】
## 反例黑名单
【TODO:不要做的事,来自真实翻车教训;单例教训先过通用化门】
@@ -0,0 +1,50 @@
{
"_说明": "LeMetaSkill 自身触发用例。增长规则:每次真实误触发/漏触发,把原话原样收编进来。",
"recommended_threshold": 0.3,
"negative_patterns": [
"不要做成 skill",
"不用做成 skill",
"只要总结",
"只解释",
"翻译一下",
"插件配置",
"斜杠命令",
"MCP 配置",
"代码 review",
"普通提示词"
],
"positive_concepts": {
"skill_artifact": ["skill", "技能", "SKILL.md", "agent skill", "能力包"],
"authoring_action": ["创建", "做个", "做一", "做成", "变成", "起草", "新建", "沉淀", "封装", "整理成", "into a skill", "turn", "create", "build", "make a skill"],
"improve_action": ["改进", "优化", "升级", "重构", "体检", "审计", "修一下", "看看", "看下", "检查", "improve", "refactor", "upgrade"],
"test_action": ["触发测试", "trigger", "结构检查", "validate", "体检"],
"material": ["流程", "工作流", "提示词", "SOP", "脚本", "方法", "清单", "workflow", "prompt", "checklist", "runbook"]
},
"required_families": ["skill_artifact"],
"families_required": 2,
"should_trigger": [
{"text": "帮我把每周写周报的流程做成一个 skill", "family": "cn_workflow_to_skill"},
{"text": "我有一套提示词,想沉淀成一个可复用的技能", "family": "cn_prompt_to_skill"},
{"text": "turn this checklist into a skill", "family": "en_checklist_to_skill"},
{"text": "给我的 skill 做个体检,看看触发灵不灵", "family": "cn_health_check"},
{"text": "这个 skill 最近总不触发,帮我改进一下", "family": "cn_improve_trigger"},
{"text": "把我们刚才调试的这个流程变成 skill 存下来", "family": "cn_from_conversation"},
{"text": "帮我看下这个 SKILL.md 写得有没有问题", "family": "cn_review_skillmd"},
{"text": "新建一个 skill,用来自动审查 API 文档", "family": "cn_create_new"},
{"text": "帮我把这个流程做成 Claude Code 的 skill", "family": "cn_claude_skill"},
{"text": "create a Codex skill for linting our docs", "family": "en_codex_skill"}
],
"should_not_trigger": [
{"text": "把这篇文章翻译成英文", "family": "translate_only"},
{"text": "帮我 review 一下这个 PR 的代码", "family": "code_review"},
{"text": "解释一下什么是 agent skill,不要做成 skill", "family": "explain_only"},
{"text": "帮我优化这段给 ChatGPT 的提示词", "family": "prompt_not_skill"},
{"text": "Claude Code 怎么安装插件", "family": "runtime_config_not_skill"},
{"text": "Summarize this note in three bullets", "family": "summary_only"}
],
"near_neighbor": [
{"text": "这个流程每次都要重复讲一遍,烦死了", "family": "vague_complaint"},
{"text": "帮我配置一个 MCP 服务器", "family": "mcp_config"},
{"text": "做个斜杠命令快速运行测试", "family": "slash_command"}
]
}
@@ -0,0 +1,47 @@
# 目录架构——资源放哪,纪律是什么
Skill 是一个目录,`SKILL.md` 是唯一入口。其余一切按"模型什么时候需要它"分层。
## 标准布局
```text
<skill-name>/
├── SKILL.md # 必需。路由 + 最简工作流,≤500 行
├── references/ # 判断类知识:方法论、决策树、评分表。正文说"做 X 前读 references/x.md"
├── scripts/ # 确定性工具:体检、测试、脚手架。能算的不让模型目测
├── evals/ # 回归用例:触发用例、输出样例。改动前后都跑它
├── assets/ # 模板、占位物:起草时复制出去改,本身不被读进上下文
└── reports/ # 运行证据:触发报告、handoff、迭代记录。给未来复查用
```
## 三层加载模型
1. `name` + `description`:常驻上下文,必须精短。
2. `SKILL.md` 正文:触发后加载,≤500 行预算。
3. `references/` 等附属文件:模型按需读取,原则上不限大小。
判断一个文件该不该存在,就看它能不能明确回答:**它提高了哪方面的稳定性?**
| 资源 | 提高的稳定性 | 不该存在的信号 |
|---|---|---|
| references/x.md | 判断质量 | 内容可以内联进正文而正文仍 <300 行 |
| scripts/x.py | 确定性、可回归 | 逻辑靠模型目测也能一次做对 |
| evals/*.json | 可回归 | skill 从不修改、或无触发风险 |
| assets/* | 起草一致性 | 从不新建同类产物 |
| reports/* | 证据、可复查 | 一次性过程记录,没人会回头看 |
## 资源纪律
- **不造仪式性目录。** 档位用不到的目录不建;scaffold 生成的空目录里要有 README 说明用途或干脆删掉。
- **不重复表达。** README 面向安装者,SKILL.md 面向模型——两份内容各说各的话,不互相复制。
- **正文引用必须可达。** 正文里出现的每个 `references/xxx.md` 链接都要真实存在(`validate_skill.py` 会检查悬空引用)。
- **单一入口。** 一个 skill 一个根 `SKILL.md`;内嵌的示例/fixture 用 `SKILL.example.md` 命名,避免被当成第二个入口发现。
- **密钥与私有路径零容忍。** 任何文件里出现 API key、token、私有绝对路径(`/Users/<你的用户名>/…`)都是 FAIL。
## 与档位的对应
- **draft**:只有 `SKILL.md`。其余都不建。
- **shared**:+ `evals/trigger_cases.json` + README(价值钩子、触发方式、安全边界三节起步)。
- **rigorous**:+ `references/` 分层 + `scripts/`(若有确定性工作)+ `reports/` 证据。
升级档位时按需补件,不提前预支。
@@ -0,0 +1,73 @@
# 交付报告——handoff 怎么写
创建或大幅修改 skill 后,用这个结构交付。报告要**短到能在最终回复里读完**,rigorous 档同步存 `reports/creation-handoff.md`(或 `iteration-<日期>.md`)。
## 报告结构
### 1. 结果
- skill 名、档位、一句话职责
- 安装路径
- 本次做了什么(创建 / 迭代了哪个面)
### 2. 参考学习(做了先例研究时)
列出 2–4 个真正看过源码的候选(搜索结果里瞥过一眼的不算):
- 名称 + URL/路径
- 为什么入选
- 学到的具体机制
- 该机制落在新 skill 的哪个位置
### 3. 取舍
- **保留**:原样吸收的机制
- **改造**:为 Leroy 的 runtime 环境(ZCode/Claude Code/Codex)改过的机制,写怎么改的
- **舍弃**:看过但没采用,写具体理由
- **原创**:先例没有、这次新造的
### 4. 亮点与证据
每个亮点三选一标注,不许裸奔:
| 标签 | 含义 | 允许的说法 |
|---|---|---|
| `[设计优势]` | 源码里看得见的设计选择 | "相比看过的候选,本包显式做了 X" |
| `[已验证]` | 有触发/输出/实测证据支撑 | "触发用例 8/8 通过(reports/trigger-eval.json)" |
| `[假设]` | 合理但未证明 | "预期能减少误触发,但缺实测证据" |
不写"最好""比 X 强"——没有公平对比就不许下比较级结论。清单差异不等于效果优势。
### 5. 验证与边界
- `validate_skill.py` 结果
- 触发测试结果(冒烟 + 实测;实测没做就写「实测证据缺失」)
- 已知的用例失败项
- 刻意排除的职责
## 紧凑模板
```markdown
已交付:<skill>(<档位>)— <一句话职责>,装在 <路径>
参考学习
- <候选A>:学到 <机制>;落在 <位置>。
- <候选B>:学到 <机制>;落在 <位置>。
取舍与原创
- 保留:… / 改造:… / 舍弃:…(理由)/ 原创:…
亮点与证据
- [设计优势] …
- [已验证] …
- [假设] …(证据缺失)
验证:体检 <PASS/WARN…>;触发 <n/n>;实测 <结果或缺失>
边界:<不做的事、已知失败项>
```
## 诚实条款
- 「证据缺失」是合法状态,写出来不丢人,编造验证才丢人。
- 计划中的工作不是证据。
- 分数是预估就标"预估",跑过实测才能去掉标注。
@@ -0,0 +1,56 @@
# 意图工坊——接单与验料
目标不是做问卷,而是避免把一个模糊想法过早包装成错误的结构。用户已经给了足够信息时,直接做,并在动手前写出自己的假设。
## 什么时候需要问
- 用户只给了粗想法,还没说明输入和输出。
- skill 可能被别人复用(档位至少 shared)。
- 触发边界和相邻请求容易混(比如"优化提示词"vs"优化 skill")。
- 用户给了参考项目、竞品或长对话,需要先圈定借鉴范围。
## 必须捕获的七要素
1. **重复任务**:它要长期接住哪一类反复出现的工作。
2. **输入**:用户实际会丢给它什么材料(文件、链接、一句话、目录……)。
3. **交付物**:做好后交回什么——文件、报告、改动、结论。
4. **排除项**:哪些相近请求**不该**触发它(负触发,写进 description)。
5. **首要标准**:速度、一致性、可审计、跨项目,哪个优先。
6. **参考**:有没有要对标的公开项目;只借结构和方法,不复制表达。
7. **已有资产**:现成的脚本、模板、旧 skill、笔记、样例在哪,能直接收编什么。
七要素里最容易被跳过的是**排除项**——而它恰恰是触发不误伤的关键。
## 中文开场方式
适合模糊请求:
> 我们先不急着定结构。你先像聊天一样说说:这个 skill 以后最想帮你稳定接住哪类重复工作?你通常会丢给它什么材料?它最后交回什么结果才算好用?
适合目标明确的请求:
> 我按可复用 skill 来做。默认假设:它要接住的重复任务是 X,输入是 Y,交付 Z,不做 A/B。假设不对的话我动手前先改。
## 停止追问的标准
当你能写出一句**可防守的能力描述**时,就停止追问:
```text
这个 skill 接收 <真实输入>,用于 <重复任务>,输出 <可交付结果>,不处理 <相邻但不该触发的请求>。
```
不要为了把结构做完整而继续问不影响设计的问题。
## 验料三问(来自工坊第一道门)
| 问题 | 通过信号 | 不通过的处置 |
|---|---|---|
| 重复性 | 这类任务已出现 ≥3 次,或可预见高频出现 | 保留为对话步骤,等重复出现后再收编 |
| 稳定性 | 流程能写成固定步骤,判断点可枚举 | 拆出稳定子流程单独做,其余留对话 |
| 增益 | 能说清比现场对话强在哪(一致性/触发/携带资产/沉淀) | 没有增益就不做,直说 |
三个都过 → 好料。部分过 → 料可用,先调整定位(缩小职责到能过三问的子集)。重复性或增益不过 → 朽木,停手,给替代方向。
## 从对话历史里提取
用户说"把我们刚才做的这个变成 skill"时,先从对话里提取:实际用的工具、步骤顺序、用户做过的纠正、输入输出格式。这些比用户事后口头描述的"理想流程"更真实——按实际发生的行为建模,再让用户确认删减。
@@ -0,0 +1,74 @@
# 迭代工坊——改进已有 Skill
改一个 skill,比新建一个更容易翻车:它已经有用户(哪怕用户只有 Leroy 自己),改坏了触发就断了。迭代的核心纪律是**可回退、可比较、单面推进**。
## 第一步:冻结基线
动任何刨子之前:
- 有 git:开分支或记录当前 commit;
- 没 git:`cp -r <skill> <项目根>/.baseline/<skill>-<日期>/`。
两条纪律(实战教训):
1. **基线必须放在发现路径之外。** 放进 `.agents/skills/` 或 `~/.claude/skills/` 里的基线副本会被 runtime 当成另一个 skill 发现,制造幽灵触发。
2. **复制完立即验证。** `head` 一下基线的 SKILL.md 确认是旧版本——有些执行环境里文件写入跨调用不可靠,没验证过的基线等于没有基线。
之后所有候选改动都与基线比。基线是裁判,不是怀旧品。
## 第二步:诊断三把尺
| 尺 | 量什么 | 怎么量 |
|---|---|---|
| 结构尺 | 写得清不清楚 | `python3 scripts/validate_skill.py <目录>`,FAIL/WARN 直接转差距条目 |
| 触发尺 | 该触发时触不触发、不该触发时误不误伤 | `python3 scripts/trigger_eval.py <目录>` + 真实对话原话抽测 |
| 活体尺 | 真实任务里跑不跑得出结果 | 拿 2–3 个真实任务实际用一遍。"看起来能触发"不等于"跑得出结果"——静默失败比文档烂致命 |
**活体尺最常被跳过,也最值钱。** 大部分真问题(输出格式其实不稳定、某个步骤总卡住、引用的脚本跑不通)只有真实运行才暴露。
## 第三步:六维评分,定位最该刨的面
每维 0–10 分,**每个分数必须给证据**(哪条用例、哪次运行、哪行源码),凭手感打分作废:
| 维度 | 权重 | 量什么 |
|---|---:|---|
| 触发与边界 | 25 | description 覆盖、负触发、误伤/漏触发记录 |
| 可执行具体性 | 20 | 步骤是否可直接照做、示例是否字面可用 |
| 工作流清晰度 | 15 | 分步结构、何时读哪个 reference |
| 失败模式与停手点 | 15 | 出错怎么办、何时问用户,有没有编码 |
| 实测表现 | 15 | 活体尺结果 |
| 资源纪律 | 10 | 有没有不承重的文件、悬空引用、超长正文 |
总分不是用来攀比的,是用来**排序**的:最低分且权重高的维度,就是本轮唯一要刨的面。
## 第四步:单面慢刨
- 一轮只改一个面(只修触发、或只重构工作流、或只补失败模式)。
- 用户批量授权("都修了吧")后,升级为**单面单提交**——每个改动独立过验证门,绝不把两个面塞进一次改动。
- 改动前先写下:本轮只刨 X;改动边界:只改 X,不动 Y;预期提升是什么。
## 第五步:验证门
候选改动**全部满足**才保留,否则回刀(恢复基线,重新想):
1. 比基线**多通过**至少 2 条测试原话(触发用例或真实任务);
2. 没有把 skill 写得更长但更难用——行数增加必须换来可执行性;
3. 没有引入私有路径、密钥、不可复现依赖;
4. 没有破坏原有通过项——基线能过而现在过不了的,是退化;
5. 通过 `validate_skill.py` 全绿。
验收时换**独立验收视角**:假装是第一次见到这个 skill 的陌生模型,只读交付物,不知道改动过程。刨子和尺子不能握在同一只手里。
## 第六步:交付与回炉
交付改动报告(改了哪个面、验证证据、行数变化),并留**回炉清单**:
- 本轮"不做"的事和理由(下一轮入口);
- 已知边界损耗(哪些用例还是过不了);
- 沉淀机会:这次手写的验证手段,值得固化成 `scripts/` 工具或 `evals/` 用例吗?值得就顺手沉淀——验证不是脚手架,是交付物的一部分。
## 迭代时机
- 用户带着"它又翻车了"的原话回来 → 先把原话收编成用例,再修。
- 隔了一个月没动 → 先跑三把尺再决定刨哪里,不要凭上次印象。
- 多次小修仍不稳 → 怀疑是定位问题(验料没做对),回到第一道门重新定位,而不是继续打补丁。
@@ -0,0 +1,45 @@
# 先例研究——搜同类,再动手
闭门造车出不了好工具。创建新 skill 或大幅重构前,先看看同类已经怎么解决了。
## 搜索策略
1. 从意图里提炼 2–4 个查询,覆盖三个角度:
- **功能词**:它做什么(如 "release notes skill")
- **形态词**:skill / agent skill / SKILL.md / Claude skill / ZCode skill
- **人群/场景词**:谁在什么场景用(如 "changelog automation")
2. 渠道:GitHub 代码搜索、WebSearch、本机已安装的 skill(`~/.agents/skills/`、`~/.zcode/skills/`、`~/.claude/skills/`、`~/.codex/skills/`、项目内目录)。用哪个渠道都行,但**每个候选必须带真实 URL 或本地路径**,不允许凭记忆说"有些项目"。
3. 搜不到就如实写"未找到相关先例"——这本身是有价值的结论(蓝海或搜索词不对),不要硬凑。
## 覆盖要求
shared 档以上,至少看 3 个候选、覆盖两类:
- **直接同行**:解决同一个问题——学机制,防同质化。
- **手艺同行**:功能不同但结构/触发/README 做得好——学手艺,不偷皮。
轻量 draft 档可跳过搜索,但要在交付报告里说明跳过理由(如"个人试验、无发布意图")。
## 检查每个候选时看什么
- `SKILL.md` 的 description 写法和正文结构(触发怎么做的)
- 渐进披露的分层(哪些进了 references/scripts)
- 维护状态、license、是否夹带私有路径或密钥(安全信号)
- 它的用户实际怎么评价(issue、README 里的真实样例)
**不要执行来历不明候选的脚本**——读源码就够了。
## 综合:keep / adapt / reject / invent
| 动作 | 含义 | 例子 |
|---|---|---|
| keep | 机制原样吸收 | 目录分层约定 |
| adapt | 机制改造成 Leroy 的环境 | 把 Claude Code 的安装流程改成 ZCode 发现路径 |
| reject | 看过但不采用,写明理由 | 重型发布流水线(职责范围外) |
| invent | 先例没有、为这个任务原创 | 验证门的具体条款 |
只借机制和结构,不拼贴上游文字、不镜像别人的仓库。每个 keep/adapt 都要在交付报告里落到具体位置("学到 X,落在 SKILL.md 第几节 / references/x.md")。
## 记录
rigorous 档把研究结论存 `reports/prior-art.md`:来源链接、日期、每个候选的取舍理由、未找到的证据缺口。搜索是当时的快照,结论要能复查。
@@ -0,0 +1,65 @@
# 触发测试——让 description 在真实对话里可靠
触发是 skill 的生命线:触发不了,写得再好也是死代码。测试分两层,先冒烟后实测。
## 第一层:冒烟测试(脚本,秒级)
```bash
python3 scripts/trigger_eval.py <skill目录>
```
脚本做三件确定性的事:
1. 解析 description,与用例文件里 `positive_concepts` 声明的**概念族词表**合并;
2. 对每条测试原话做概念族匹配:命中负触发词直接判不触发;否则需命中全部 `required_families`(如 skill 实体族——"优化提示词"没有 skill 就不该触发)且总命中族数 ≥ `families_required`(默认 2,防止单个词误伤);同时计算词面覆盖率作参考信息;
3. 输出 `reports/trigger-eval.json`(每条 case 的判定、命中族、覆盖率)。
它是**词面信号检查**,不是行为判断——作用是快速暴露"description 里根本没写用户会说的词"这类硬伤。没有配置概念族的用例文件会退化为纯覆盖率阈值模式。
## 第二层:真实对话实测(必做)
冒烟全绿不代表会触发。在**新的对话轮**里,把测试原话当真实请求发给模型,观察:
- 该触发的原话,skill 有没有被加载、有没有按正文执行;
- 不该触发的原话,有没有误伤。
每档至少测:
- **应触发** 3–5 条:覆盖不同说法(口语/正式/中英混合)、不同入口(直接请求/丢来材料/求助式提问)。
- **不应触发** 2–3 条:相邻请求("优化这段提示词"vs"优化这个 skill")、一次性任务。
- **边界** 1–2 条:模糊但倾向触发的("这个流程每次都要重复讲一遍,烦死了")。
## 用例文件设计
`evals/trigger_cases.json` 的用例来自**用户真的会说的话**,不是设计师的语言。写用例时:
```json
{
"negative_patterns": ["不要做成 skill", "只要总结", "翻译一下"],
"should_trigger": [
{"text": "帮我把每周的周报整理做成一个 skill", "family": "cn_workflow"},
{"text": "turn this checklist into a skill", "family": "en_workflow"}
],
"should_not_trigger": [
{"text": "把这篇文章翻译成英文", "family": "translate_only"}
],
"near_neighbor": [
{"text": "帮我优化这段提示词", "family": "prompt_not_skill"}
]
}
```
- 每条标注 `family`,回归时按族看退化。
- 用例随真实误触发/漏触发事件增长:每次现场翻车,把那句话原样收编成用例——这是触发资产的本金。
- `near_neighbor` 是最值钱的一族:它们定义了边界。
## 判定与修复
- 漏触发 → description 缺场景词或写得太被动 → 补触发短语,重跑。
- 误触发 → 缺负触发 → 在 description 加"不要用于……",重跑。
- 修 description 而不是加规则正文——触发问题永远先在触发信号上修。
- 改完 description 必须全量重跑用例,防止按下葫芦浮起瓢。
## 记录
rigorous 档把实测结果记进 `reports/trigger-eval.json` 的 `live_turns` 段:每条原话、期望、实际、结论。没做实测就标注「实测证据缺失」,不假装做过。
@@ -0,0 +1,56 @@
# 写作手艺——description 与正文
Skill 的两个加载层各有各的写法:description 决定被不被触发,正文决定触发后好不好执行。
## description:唯一的自动触发信号
各 runtime(ZCode / Claude Code / Codex)都靠 frontmatter 的 `description` 决定何时加载 skill(连同 `name` 常驻上下文)。模型倾向于**漏触发**而不是误触发,所以 description 要写得主动一点。
**四件必须写进去的事:**
1. **做什么**:一句话职责。
2. **何时触发**:用户真实会说的话——"帮我做个 skill""把这个流程沉淀下来""给我的 skill 做体检"。给 3–8 条原话级别的触发词。
3. **负触发**:明确"不要用于……"的相邻请求。没有负触发的 description 会在边界请求上犹豫。
4. **丢来就触发**的说明:用户丢来 SKILL.md、目录、链接说"帮我看看"时该不该接。
**写法检查:**
- 用中英混合的真实口语测试:拿 5 条用户会说的原话(含 2 条不该触发的)逐条对照 description,每条都该能明确判出触发/不触发。
- 别写成功能清单("支持 A、B、C"),写成场景句("当用户想……时使用")。
- 长度:3–8 行。太短盖不住场景,太长稀释信号。
## 正文:给模型的操作手册
正文只在触发后加载,目标是让模型照着执行,而不是让人读了佩服。
**七条手艺:**
1. **祈使句**。"读取文件后再编辑",不是"你应该考虑读取文件"。
2. **讲为什么**。规则不明显时给理由——模型理解了原因才会在边界情况做对判断。发现自己在写全大写 MUST 时,通常说明该把理由讲清楚,而不是加重语气。
3. **示例胜过规则**。要求结构化输出就贴一个字面示例;要求调某工具就给调用样例。一段示例顶三条规则。
4. **正文 ≤500 行**。超了就把领域细节拆进 references,正文里留一句"做 X 前先读 references/x.md"。
5. **每节一个职责**。读者跳着读不迷路。
6. **失败模式要编码**。"遇到 X 时停下来问用户"比"小心行事"有用得多。
7. **写完删一遍**。删掉不承重的句子。规则越少,剩下的越有权威。
## 跨 runtime 约定(ZCode / Claude Code / Codex)
Skill 遵循 Agent Skills 开放格式:一个目录 + `SKILL.md`(frontmatter:`name` + `description`)。三个 runtime 都认这个格式,差别只在发现路径:
| runtime | 项目级 | 用户级 |
|---|---|---|
| ZCode | `.zcode/skills/`(优先)· `.agents/skills/` | `~/.zcode/skills/` · `~/.agents/skills/` |
| Claude Code | `.claude/skills/` | `~/.claude/skills/` |
| Codex | `.codex/skills/` | `~/.codex/skills/`(实验性) |
- **frontmatter**:`name`(kebab-case,1–64 字符,必须与目录名一致)+ `description` 是必填;其余保留字段按需。
- **新建默认放 `.agents/skills/`**——跨工具标准位;目标 runtime 不读这个位置时,复制或软链到它的目录(一份源,多处入口,改完同步)。
- **目录即身份**:同名 skill 不同路径算两个安装,ZCode 里路径优先级高的胜出——起名字前先查各 runtime 的发现路径有没有撞名。
- **可移植三禁**:正文不写死某个 runtime 专属的工具名/技能名(如把"配置排障"指给单一 runtime 的指南);不用私有绝对路径;脚本只用 python3/bash 标准库。做到这三条,同一个目录在三个 runtime 里行为一致。
- **先例研究时的提示**:搜 Claude 生态的 skill(数量最多)借鉴机制没问题,但安装路径、触发语法要按目标 runtime 换算,不要照搬。
## 命名
- 英文 kebab-case,≤3 个连字符段为佳:`weekly-report`、`api-docs-lint`。
- 名字要能自解释职责;人设名(如 `luban`)只有配合强 description 才可用。
- 与已有 skill 撞名 = 改名,不是覆盖。
+10
View File
@@ -0,0 +1,10 @@
# reports/ —— LeMetaSkill 的运行证据
本目录存放可复查的运行证据,供迭代时对照:
- `trigger-eval.json` —— 自身触发冒烟测试输出(`live_turns` 段手工补记真实对话实测结果)
- `prior-art.md` —— 先例研究结论(来源、日期、keep/adapt/reject/invent 取舍)
- `creation-handoff.md` —— 创建交付报告
- `iteration-<日期>.md` —— 每轮迭代的改动报告与回炉清单
诚实条款:验证不了的写「证据缺失」;计划中的工作不是证据;预估分数标"预估"。
@@ -0,0 +1,36 @@
# LeMetaSkill 创建交付报告
日期:2026-10-05。创建路径:用户提出需求 → 确认四项设计决策(范围=创建+迭代;体量=重工程;命名=LeMetaSkill;位置=本项目)→ 研究两个先例仓库 → 起草 → 自检迭代 → 交付。
## 1. 结果
- **skill**:`le-meta-skill` v1.0.0,rigorous 档
- **一句话职责**:把重复出现的方法变成可触发、可验证、可维护的 ZCode Skill,并负责已有 Skill 的迭代改进。
- **路径**:`<仓库>/skills/le-meta-skill/`(独立 git 仓库,v1.1.0 起从项目 `.agents/` 迁出)
- **结构**:SKILL.md 127 行 + 7 个 references(401 行)+ 3 个脚本 + 模板 + 自身触发用例 + 证据目录
## 2. 参考学习
见 [prior-art.md](prior-art.md):乔木(生命周期流水线与证据纪律)、鲁班(工匠方法论与停手点)、skill-creator(宿主基线约定)。每个候选的取舍均落到了具体文件。
## 3. 亮点与证据
- **[已验证]** 结构体检全绿:`validate_skill.py` 自检 PASS:10 / WARN:0 / FAIL:0(含 frontmatter、命名一致、悬空引用、密钥扫描、目录用途)。
- **[已验证]** 触发冒烟 13/13 一致(8 应触发 + 5 不应触发),3 条边界不计分;近邻判别正确("帮我优化这段给 ChatGPT 的提示词"因缺 skill_artifact 族被正确拒绝)。报告:[trigger-eval.json](trigger-eval.json)。
- **[已验证]** scaffold 端到端验证:生成的 shared 档骨架通过全项体检,用例模板 JSON 可解析(首版模板有 `{{` 转义 bug,体检当场抓住后修复)。
- **[设计优势]** 相比看过的三个候选,本包是唯一面向 ZCode 发现路径/优先级/覆盖语义适配的 meta skill;且 scaffold→validate→trigger_eval 出生即合规流水线为三家方法的首次组合。
- **[假设]** 六维评分与验证门条款能提高迭代质量——方法吸收自鲁班实战报告,但本 skill 尚无自己的迭代轮次证明。
## 4. 验证与边界
- 已验证:三个脚本功能正常(自检 + 临时目录端到端)。
- **实测证据缺失**:真实对话触发行为未测——需要在新会话中用 `evals/trigger_cases.json` 的原话实测(新建会话后 skill 才会进入发现列表)。此项补齐前,触发可靠性只能标 [假设]。
- 已知边界:trigger_eval 是词面检查不是行为判断(脚本与 reference 中均已声明);中文分词为二元组近似,概念族词表需要随误触发案例增长。
- 刻意排除:发布流水线、Skill IR、README 产品页工程、showcase 资产(职责范围外)。
## 5. 回炉清单
1. 用新会话实测触发(补 trigger-eval.json 的 live_turns 段);
2. 第一次真实使用它创建一个 skill,回填流程中不顺手的步骤;
3. 迭代纪律的单面慢刨尚未经实战检验;
4. 概念族词表随每次真实误触发/漏触发增长。
@@ -0,0 +1,47 @@
# LeMetaSkill 迭代报告 · 2026-10-05(v1.0.0 → v1.1.0)
本轮驱动:用户反馈两条——(1) skill 不应绑死 ZCode,要能在 Claude Code 和 Codex 里用;(2) 用它自己迭代它自己。第 (1) 条即本轮唯一改动面;第 (2) 条决定了流程按 [迭代工坊](../references/iteration.md) 走。
## 本轮只刨:runtime 中性化
改动边界:只改与 runtime 定位相关的措辞与约定,不动工作流结构、不动脚本算法、不动其余 references。
### 建议文件变更(实际执行)
| 文件 | 操作 | 内容 |
|---|---|---|
| SKILL.md | 修改 | description:"创建与迭代 ZCode 技能"→"创建与迭代 Agent Skill,ZCode / Claude Code / Codex 等 runtime 通用";负触发去掉 zcode-guide 专属指向;总则"内置 skill-creator"→"宿主内置";metadata 增 `runtimes` 字段,版本 1.0.0→1.1.0 |
| references/writing-craft.md | 修改 | "ZCode 专属约定"整节重写为"跨 runtime 约定":三 runtime 发现路径表、`.agents/skills/` 默认位 + 复制/软链策略、可移植三禁(不写死 runtime 专属工具名/无私有路径/脚本仅标准库) |
| references/prior-art.md | 修改 | 本机已装 skill 的查找渠道补 `~/.claude/skills/`、`~/.codex/skills/` |
| references/handoff.md | 修改 | 取舍示例去掉 ZCode 绑定措辞 |
| evals/trigger_cases.json | 修改 | 新增 3 条用例:应触发 ×2("做成 Claude Code 的 skill"、"create a Codex skill"),不应触发 ×1("Claude Code 怎么安装插件"——配置求助不是 skill 创作) |
| reports/creation-handoff.md | 修改 | 私有绝对路径改项目相对路径(体检 WARN 抓出的疏漏) |
| references/iteration.md | 修改 | 沉淀本轮教训:基线放发现路径之外、复制后立即验证(独立第二改动,按单面单提交纪律与本面分开) |
### 六维评分(迭代前 → 迭代后,预估)
| 维度 | 权重 | 前 | 后 | 证据 |
|---|---:|---:|---:|---|
| 触发与边界 | 25 | 7 | 9 | 活体尺:用户反馈暴露定位缺陷(description 写死 ZCode);修后跨 runtime 用例 4/4 通过 |
| 可执行具体性 | 20 | 8 | 8 | 未动(本轮不改流程);跨 runtime 路径表给了明确操作 |
| 工作流清晰度 | 15 | 8 | 8 | 未动 |
| 失败模式与停手点 | 15 | 7 | 7 | 未动;"可移植三禁"补了一种失败模式 |
| 实测表现 | 15 | 6 | 7 | 冒烟 16/16;真实对话实测仍缺失(见边界) |
| 资源纪律 | 10 | 9 | 9 | 体检全绿;抓出并修复 1 处私有路径 |
## 验证门(全部满足)
1. ✅ 比基线多过 3 条测试原话(13→16 条计分用例全过,含跨 runtime 新用例)
2. ✅ SKILL.md 127→128 行,writing-craft.md 46→56 行——增长全部用于路径表与可移植规则,可执行性增加
3. ✅ 无私有路径、无密钥、无不可复现依赖(体检 WARN 修复后全绿)
4. ✅ 基线通过项无退化(原有 13 条用例仍全过)
5. ✅ `validate_skill.py` PASS:10 / WARN:0 / FAIL:0;`trigger_eval.py` 16/16 一致(报告:[trigger-eval.json](trigger-eval.json))
基线:`<项目根>/.baseline/le-meta-skill-2026-10-05/`(已验证为 v1.0.0 内容,可回滚)。
## 回炉清单
1. **真实对话实测**(连续两轮欠的账):在 ZCode 新会话测触发;复制/软链到 `~/.claude/skills/` 与 `~/.codex/skills/` 后各自实测一次——跨 runtime 可移植性目前是 [设计优势],实测后才升 [已验证]。
2. Codex skills 支持仍是实验性(2025-12 起),路径约定可能变;下次回炉时复核官方文档。
3. 本轮不做:触发判定算法调优、六维评分权重校准、references 进一步拆分——都没有失败证据驱动,不为改而改。
4. 环境教训(已沉淀进 iteration.md):本工作区执行沙箱的文件写入跨调用不可靠,基线复制后必须当场验证。
+58
View File
@@ -0,0 +1,58 @@
# 先例研究:LeMetaSkill v1.0.0
研究日期:2026-10-05。研究方式:浅 clone 两个仓库,通读 SKILL.md、references、scripts、evals 全部源文件。
## 候选
### 1. joeseesun/qiaomu-meta-skill(直接同行)
- 来源:https://github.com/joeseesun/qiaomu-meta-skill
- 定位:英文为主的"Skill OS"——研究、创建、改进、评估、打包、治理、发布 qiaomu 风格 skill 的全生命周期流水线,v2.8.x。
- 为什么入选:与本 skill 职责重叠最大的公开实现,工程化程度最高。
- 维护信号:版本迭代到 2.8.1,含 tests/、reports/ 真实迭代记录(2026-07/08)。
### 2. LearnPrompt/luban-skill(直接同行 + 手艺同行)
- 来源:https://github.com/LearnPrompt/luban-skill
- 定位:中文工匠人设的"Skill 打磨工坊"——把已有 skill 打磨成可传播的公共资产,五动作方法论(验料/访行/过尺/慢刨/回炉)。
- 为什么入选:明确**不做从零创建**(官方建议用 skill-creator),恰好与本项目互补;其中文输出契约、停手点纪律是三家中最强的。
### 3. ZCode 内置 skill-creator(手艺同行)
- 来源:ZCode 官方插件(本地缓存),非公开仓库
- 定位:通用 skill 创作循环——意图捕获、起草、测试 prompt、迭代。
- 为什么入选:宿主环境的官方基线;任何个性化 meta skill 都必须先吃透它的目录约定和加载模型。
## keep / adapt / reject / invent
**保留(keep)**
- 单一创建权威规则(触发后不再调 skill-creator,防双重初始化)→ 落在 SKILL.md 工坊总则。
- "写准不写长"、先验证触发再扩内容、description 先行 → SKILL.md 总则 + writing-craft.md。
- 渐进披露三层加载模型 → writing-craft.md + architecture.md。
- 通用化门(单例教训两个领域复现才进核心规则)→ SKILL.md 第二道门。
- 证据三级标签 [设计优势]/[已验证]/[假设] 与"证据缺失"诚实条款 → handoff.md。
- 概念族触发判定(required_families + families_required)→ trigger_eval.py。首版用纯覆盖率算法,冒烟测试自己暴露 5 处误判后改用此法——这是先例方法在实战中优于自研首版的直接证据。
**改造(adapt)**
- 乔木四档(Scaffold/Production/Library/Governed)→ 三档(draft/shared/rigorous):砍掉发布治理档(职责范围外),把发布相关的门禁全部移除,只保留创建+迭代。
- 乔木触发评估(英文概念词典内嵌脚本)→ 中英双语 + 用例文件声明概念族 + 负触发词覆盖判定,且实测分层明确写成"冒烟≠行为"。
- 鲁班九维评分(100 分制,面向发布打磨)→ 六维(触发与边界/可执行具体性/工作流清晰度/失败模式/实测/资源纪律),删去传播类维度(安装率/showcase),保留"每个分数必须给证据"。
- 鲁班冻结基线 + 验证门 + 单面慢刨 + 独立验收视角 → iteration.md 全套吸收,"commit 即 push"改为可回退基线(本环境无 git 约定)。
- 鲁班出生证清单/README house 模板 → 简化为 shared 档 README 三节起步(价值/触发/安全边界),发布类必备件(marketplace.json、GIF、徽章)不采用。
**舍弃(reject)**
- GitHub 发布流水线(feature branch → PR → release → npx 安装验证):用户明确职责范围为创建+迭代,不含发布。
- Skill IR 导出、agents/interface.yaml、manifest.json:面向跨平台发布的中间表示,当前无此需求;若未来要发布可再引入。
- 鲁班的"出师证书"结果卡、README 十条铁律、showcase 优先级:面向公共资产传播,自用 skill 不需要。
- 乔木的 prior-art 双目录搜索脚本(skills.sh/SkillsMP API):搜索渠道随时会变,改为方法论 reference + 人工搜索,工具不绑定特定目录服务。
**原创(invent)**
- ZCode 专属适配:发现路径优先级(.zcode/skills > .agents/skills,同名覆盖语义)、frontmatter 最小契约、正文 ≤500 行预算——三份先例均面向 Claude Code 系生态。
- validate_skill.py 的悬空引用检查(正文引用的 references/*.md 必须存在)+ 私有路径拼接式扫描(避免扫描器匹配自身源码,实测踩过的坑)。
- scaffold→validate→trigger_eval 的端到端出生即合规流水线(乔木有 validate 但无 scaffold;鲁班有 scaffold 但无 trigger eval)。
- 验证资产沉淀问题("这次的验证手段能不能留下来?")写进迭代交付步骤。
## 未找到的证据
- 未找到专门面向 ZCode(而非 Claude Code)生态的公开 meta skill——三家先例全部以 Claude Code/skills.sh 生态为主。此为差异化空位,也是本 skill 的存在理由之一。
- 各先例的触发实测数据(live-turn 结果)均未公开,只有词面/结构检查;本 skill 的实测同样待补(见 creation-handoff.md 的证据缺失声明)。
@@ -0,0 +1,380 @@
{
"skill": "le-meta-skill",
"date": "2026-10-05",
"threshold": 0.3,
"prediction_mode": "concept_families",
"note": "词面冒烟测试;行为结论需真实对话实测(live_turns 字段手工补记)",
"cases": [
{
"text": "帮我把每周写周报的流程做成一个 skill",
"family": "cn_workflow_to_skill",
"bucket": "should_trigger",
"coverage": 0.231,
"coverage_hits": [
"skill",
"做成",
"流程"
],
"families_hit": [
"authoring_action",
"material",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "我有一套提示词,想沉淀成一个可复用的技能",
"family": "cn_prompt_to_skill",
"bucket": "should_trigger",
"coverage": 0.562,
"coverage_hits": [
"可复",
"复用",
"技能",
"提示",
"沉淀",
"淀成",
"用的",
"的技",
"示词"
],
"families_hit": [
"authoring_action",
"material",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "turn this checklist into a skill",
"family": "en_checklist_to_skill",
"bucket": "should_trigger",
"coverage": 1.0,
"coverage_hits": [
"checklist",
"into",
"skill",
"this",
"turn"
],
"families_hit": [
"authoring_action",
"material",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "给我的 skill 做个体检,看看触发灵不灵",
"family": "cn_health_check",
"bucket": "should_trigger",
"coverage": 0.75,
"coverage_hits": [
"skill",
"不灵",
"个体",
"体检",
"做个",
"我的",
"看看",
"给我",
"触发"
],
"families_hit": [
"authoring_action",
"improve_action",
"skill_artifact",
"test_action"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "这个 skill 最近总不触发,帮我改进一下",
"family": "cn_improve_trigger",
"bucket": "should_trigger",
"coverage": 0.333,
"coverage_hits": [
"skill",
"改进",
"触发"
],
"families_hit": [
"improve_action",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "把我们刚才调试的这个流程变成 skill 存下来",
"family": "cn_from_conversation",
"bucket": "should_trigger",
"coverage": 0.286,
"coverage_hits": [
"skill",
"个流",
"变成",
"流程"
],
"families_hit": [
"authoring_action",
"material",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "帮我看下这个 SKILL.md 写得有没有问题",
"family": "cn_review_skillmd",
"bucket": "should_trigger",
"coverage": 0.4,
"coverage_hits": [
"md",
"skill",
"我看",
"看下"
],
"families_hit": [
"improve_action",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "新建一个 skill,用来自动审查 API 文档",
"family": "cn_create_new",
"bucket": "should_trigger",
"coverage": 0.2,
"coverage_hits": [
"skill",
"新建"
],
"families_hit": [
"authoring_action",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "帮我把这个流程做成 Claude Code 的 skill",
"family": "cn_claude_skill",
"bucket": "should_trigger",
"coverage": 0.778,
"coverage_hits": [
"claude",
"code",
"skill",
"个流",
"做成",
"把这",
"流程"
],
"families_hit": [
"authoring_action",
"material",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "create a Codex skill for linting our docs",
"family": "en_codex_skill",
"bucket": "should_trigger",
"coverage": 0.429,
"coverage_hits": [
"codex",
"create",
"skill"
],
"families_hit": [
"authoring_action",
"skill_artifact"
],
"negative_hit": false,
"predicted_trigger": true,
"expected_trigger": true,
"verdict": "pass"
},
{
"text": "把这篇文章翻译成英文",
"family": "translate_only",
"bucket": "should_not_trigger",
"coverage": 0.111,
"coverage_hits": [
"把这"
],
"families_hit": [],
"negative_hit": false,
"predicted_trigger": false,
"expected_trigger": false,
"verdict": "pass"
},
{
"text": "帮我 review 一下这个 PR 的代码",
"family": "code_review",
"bucket": "should_not_trigger",
"coverage": 0.4,
"coverage_hits": [
"review",
"代码"
],
"families_hit": [],
"negative_hit": false,
"predicted_trigger": false,
"expected_trigger": false,
"verdict": "pass"
},
{
"text": "解释一下什么是 agent skill,不要做成 skill",
"family": "explain_only",
"bucket": "should_not_trigger",
"coverage": 0.375,
"coverage_hits": [
"agent",
"skill",
"做成"
],
"families_hit": [
"authoring_action",
"skill_artifact"
],
"negative_hit": true,
"predicted_trigger": false,
"expected_trigger": false,
"verdict": "pass"
},
{
"text": "帮我优化这段给 ChatGPT 的提示词",
"family": "prompt_not_skill",
"bucket": "should_not_trigger",
"coverage": 0.333,
"coverage_hits": [
"优化",
"提示",
"示词"
],
"families_hit": [
"improve_action",
"material"
],
"negative_hit": false,
"predicted_trigger": false,
"expected_trigger": false,
"verdict": "pass"
},
{
"text": "Claude Code 怎么安装插件",
"family": "runtime_config_not_skill",
"bucket": "should_not_trigger",
"coverage": 0.5,
"coverage_hits": [
"claude",
"code",
"插件"
],
"families_hit": [],
"negative_hit": false,
"predicted_trigger": false,
"expected_trigger": false,
"verdict": "pass"
},
{
"text": "Summarize this note in three bullets",
"family": "summary_only",
"bucket": "should_not_trigger",
"coverage": 0.167,
"coverage_hits": [
"this"
],
"families_hit": [],
"negative_hit": false,
"predicted_trigger": false,
"expected_trigger": false,
"verdict": "pass"
},
{
"text": "这个流程每次都要重复讲一遍,烦死了",
"family": "vague_complaint",
"bucket": "near_neighbor",
"coverage": 0.25,
"coverage_hits": [
"个流",
"流程",
"重复"
],
"families_hit": [
"material"
],
"negative_hit": false,
"predicted_trigger": false,
"expected_trigger": null,
"verdict": "boundary"
},
{
"text": "帮我配置一个 MCP 服务器",
"family": "mcp_config",
"bucket": "near_neighbor",
"coverage": 0.333,
"coverage_hits": [
"mcp",
"配置"
],
"families_hit": [],
"negative_hit": false,
"predicted_trigger": false,
"expected_trigger": null,
"verdict": "boundary"
},
{
"text": "做个斜杠命令快速运行测试",
"family": "slash_command",
"bucket": "near_neighbor",
"coverage": 0.455,
"coverage_hits": [
"做个",
"命令",
"斜杠",
"杠命",
"测试"
],
"families_hit": [
"authoring_action"
],
"negative_hit": true,
"predicted_trigger": false,
"expected_trigger": null,
"verdict": "boundary"
}
],
"summary": {
"scored": 16,
"mismatch": 0,
"pass_rate": 1.0
}
}
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""LeMetaSkill 起草脚手架:按档位生成"出生即合规"的 skill 骨架。
档位与产出:
- draft 仅 SKILL.md(来自 assets/SKILL.template.md)
- shared + evals/trigger_cases.json 用例模板 + README.md(三节起步)
- rigorous + reports/ 证据目录约定
用法: python3 scaffold.py <name> "<一句话职责>" [--tier draft|shared|rigorous] [--target 目录]
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
TIERS = ("draft", "shared", "rigorous")
README_TEMPLATE = """# {name}
> {tagline}
**状态**:{tier} 档,个人/项目内使用。
## 它解决什么问题
【TODO:2-3 句,第二人称说清痛点和交付物】
## 触发方式
- 【TODO:用户真实会说的原话 3-8 条】
## 安全边界
【TODO:不会做什么、什么时候停下来问用户】
"""
REPORTS_README = """# reports/ —— 运行证据
存放本 skill 的可复查证据,约定:
- `trigger-eval.json` trigger_eval.py 的输出(含手工补记的 live_turns 实测段)
- `creation-handoff.md` 创建交付报告(rigorous 档)
- `iteration-<日期>.md` 每轮迭代的改动报告与回炉清单
诚实条款:验证不了的写「证据缺失」,不编造;计划中的工作不是证据。
"""
CASES_TEMPLATE = """{
"_说明": "填入用户真实会说的话:应触发/不应触发/边界各若干条。family 标注场景族,便于回归分析。",
"recommended_threshold": 0.3,
"negative_patterns": ["不要做成 skill", "只要总结", "翻译一下"],
"should_trigger": [
{"text": "【TODO:用户原话】", "family": "cn_case_1"}
],
"should_not_trigger": [
{"text": "【TODO:相邻但不该触发的原话】", "family": "near_task"}
],
"near_neighbor": []
}
"""
def main() -> int:
ap = argparse.ArgumentParser(description="生成 skill 骨架")
ap.add_argument("name")
ap.add_argument("tagline")
ap.add_argument("--tier", choices=TIERS, default="draft")
ap.add_argument("--target", type=Path, default=None)
args = ap.parse_args()
name = args.name.strip().lower()
if not re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", name):
print(f"✘ name 必须是 kebab-case(小写字母数字+连字符): {name!r}")
return 1
target = (args.target or Path(".agents/skills") / name).resolve()
if target.exists() and any(target.iterdir()):
print(f"✘ 目标目录已存在且非空: {target}(不覆盖,确认后手动清理)")
return 1
target.mkdir(parents=True)
# 模板:优先读 assets/SKILL.template.md,缺失时用内置兜底
template_path = Path(__file__).resolve().parent.parent / "assets" / "SKILL.template.md"
if template_path.is_file():
template = template_path.read_text(encoding="utf-8")
else:
template = (
"---\nname: {name}\ndescription: |\n {tagline}\n 【TODO:做什么/何时触发,2-3 句 + 触发原话】\n"
" 不要用于:【TODO:负触发,至少 2 条】。\n---\n\n# {name}\n\n【TODO:工作流正文】\n"
)
(target / "SKILL.md").write_text(
template.replace("{{name}}", name).replace("{{tagline}}", args.tagline),
encoding="utf-8",
)
print(f"✔ {target / 'SKILL.md'}")
if args.tier in ("shared", "rigorous"):
cases_dir = target / "evals"
cases_dir.mkdir(exist_ok=True)
(cases_dir / "trigger_cases.json").write_text(CASES_TEMPLATE, encoding="utf-8")
print(f"✔ {cases_dir / 'trigger_cases.json'}(待填真实原话)")
(target / "README.md").write_text(
README_TEMPLATE.format(name=name, tagline=args.tagline, tier=args.tier), encoding="utf-8"
)
print(f"✔ {target / 'README.md'}")
if args.tier == "rigorous":
reports = target / "reports"
reports.mkdir(exist_ok=True)
(reports / "README.md").write_text(REPORTS_README, encoding="utf-8")
print(f"✔ {reports / 'README.md'}")
print(
f"\n下一步:\n"
f"1. 填 TODO(grep -rn 'TODO' {target})——先写好 description 再写正文;\n"
f"2. python3 scripts/validate_skill.py {target};\n"
f"3. 填好用例后 python3 scripts/trigger_eval.py {target},再做真实对话实测。"
)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""LeMetaSkill 触发冒烟测试:词面检查 description 是否覆盖用户会说的原话。
这是**词面信号检查**,不是行为判断——它快速暴露"description 里没写用户
会用的词"这类硬伤;真正的触发行为要靠真实对话实测(见 references/trigger-eval.md)。
做法:
1. 解析 SKILL.md 的 description,与用例文件 positive_concepts 的概念族词表合并;
2. 对每条测试原话做**概念族匹配**:负触发词命中直接判不触发;否则需命中全部
required_families 且总命中族数 >= families_required(无概念族配置时退化为覆盖率阈值);
3. 与期望比对,报告不一致条目;结果连同词面覆盖率写入 reports/trigger-eval.json。
用法: python3 trigger_eval.py <skill目录> [--cases 路径] [--output 路径] [--threshold 0.3]
退出码: 0=全部一致 1=有不一致或 FAIL 2=缺少用例文件
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import date
from pathlib import Path
CJK = r"\u4e00-\u9fff"
LATIN_WORD = re.compile(r"[a-z0-9]+")
CJK_RUN = re.compile(rf"[{CJK}]+")
# 中文虚词/口语助词:不承重的触发信号,剔除后再匹配
STOP_BIGRAMS = {
"我们", "你们", "帮我", "帮忙", "一下", "这个", "那个", "一个", "一些",
"然后", "可能", "可以", "应该", "就是", "还是", "什么", "怎么", "如果",
"但是", "所以", "觉得", "感觉", "一下", "每次", "先不", "不用", "不要",
"没有", "已经", "正在", "出来", "上去", "过去", "东西", "事情", "麻烦",
}
STOP_SINGLE = set("的了吧呢啊呀嘛么是有在和与我你他她它就把被这那也给从对上下里中要去会来能多很太都还再又就才只")
NEG_HINT = re.compile(r"不要|不该|不用于|排除|exclude|not for|do not use|don'?t use", re.I)
def normalize(text: str) -> str:
return re.sub(r"\s+", " ", text.lower().strip())
def tokenize(text: str) -> set[str]:
"""中文 → 滑动二元组(剔除虚词);英文/数字 → 单词。"""
tokens: set[str] = set()
for run in CJK_RUN.findall(text):
if len(run) == 1:
if run not in STOP_SINGLE:
tokens.add(run)
continue
for i in range(len(run) - 1):
bg = run[i : i + 2]
if bg not in STOP_BIGRAMS:
tokens.add(bg)
for w in LATIN_WORD.findall(text):
if len(w) > 1:
tokens.add(w)
return tokens
def parse_description(skill_md: Path) -> tuple[str, dict[str, str]]:
text = skill_md.read_text(encoding="utf-8")
lines = text.splitlines()
fields: dict[str, str] = {}
if lines and lines[0].strip() == "---":
key = ""
for line in lines[1:60]:
if line.strip() == "---":
break
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
if m:
key = m.group(1)
fields[key] = m.group(2).strip().strip("'\"")
elif line.startswith((" ", "\t")) and key:
fields[key] = (fields[key] + " " + line.strip()).strip()
return fields.get("description", ""), fields
def coverage(case_text: str, desc_tokens: set[str]) -> tuple[float, list[str]]:
case_tokens = tokenize(normalize(case_text))
if not case_tokens:
return 0.0, []
hits = sorted(t for t in case_tokens if t in desc_tokens)
return len(hits) / len(case_tokens), hits
def phrase_in(phrase_norm: str, text_norm: str) -> bool:
"""短语命中:含中文按子串,纯拉丁按整词边界。"""
if re.search(rf"[{CJK}]", phrase_norm):
return phrase_norm in text_norm
return re.search(rf"\b{re.escape(phrase_norm)}\b", text_norm) is not None
def family_hits(text_norm: str, concepts: dict[str, list[str]]) -> dict[str, str]:
"""返回 {命中的族: 命中的短语}。"""
hits: dict[str, str] = {}
for family, phrases in concepts.items():
for p in phrases:
pn = normalize(p)
if pn and phrase_in(pn, text_norm):
hits[family] = p
break
return hits
def load_cases(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def as_items(raw) -> list[dict]:
items = []
for entry in raw or []:
if isinstance(entry, str):
items.append({"text": entry, "family": "default"})
elif isinstance(entry, dict):
it = dict(entry)
it.setdefault("family", "default")
items.append(it)
return items
def main() -> int:
ap = argparse.ArgumentParser(description="触发冒烟测试")
ap.add_argument("skill_dir", type=Path)
ap.add_argument("--cases", type=Path, default=None)
ap.add_argument("--output", type=Path, default=None)
ap.add_argument("--threshold", type=float, default=None)
args = ap.parse_args()
root = args.skill_dir.resolve()
skill_md = root / "SKILL.md"
if not skill_md.is_file():
print(f"FAIL SKILL.md 不存在: {skill_md}")
return 1
cases_path = (args.cases or root / "evals" / "trigger_cases.json").resolve()
if not cases_path.is_file():
cases_path.parent.mkdir(parents=True, exist_ok=True)
starter = {
"_说明": "填入用户真实会说的话:应触发/不应触发/边界各若干条。family 标注场景族,便于回归分析。",
"recommended_threshold": 0.3,
"negative_patterns": ["不要做成 skill", "只要总结", "翻译一下"],
"should_trigger": [
{"text": "【TODO:用户原话】", "family": "cn_workflow"},
],
"should_not_trigger": [
{"text": "【TODO:相邻但不该触发的原话】", "family": "near_task"},
],
"near_neighbor": [],
}
cases_path.write_text(json.dumps(starter, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"未找到用例文件,已生成模板: {cases_path}")
print("请填入真实用例后重跑。")
return 2
cases = load_cases(cases_path)
desc, _fields = parse_description(skill_md)
if not desc:
print("FAIL description 为空,触发信号不存在")
return 1
if not NEG_HINT.search(desc):
print("WARN description 缺负触发表述,不应触发的判定仅靠用例负词兜底")
threshold = args.threshold if args.threshold is not None else float(cases.get("recommended_threshold", 0.3))
desc_tokens = tokenize(normalize(desc))
# positive_concepts: {族: [词]},视作 description 的延伸词面
concepts = cases.get("positive_concepts")
if isinstance(concepts, dict):
for words in concepts.values():
for w in words:
desc_tokens |= tokenize(normalize(w))
neg_patterns = [normalize(p) for p in cases.get("negative_patterns", [])]
concepts = cases.get("positive_concepts") if isinstance(cases.get("positive_concepts"), dict) else {}
required_families = set(cases.get("required_families", []))
families_required = int(cases.get("families_required", 2))
use_families = bool(concepts)
report: dict = {
"skill": root.name,
"date": str(date.today()),
"threshold": threshold,
"prediction_mode": "concept_families" if use_families else "coverage_fallback",
"note": "词面冒烟测试;行为结论需真实对话实测(live_turns 字段手工补记)",
"cases": [],
}
n_mismatch = n_total = 0
for bucket, expected in (("should_trigger", True), ("should_not_trigger", False), ("near_neighbor", None)):
for item in as_items(cases.get(bucket)):
text = normalize(item["text"])
hit_neg = any(p and p in text for p in neg_patterns)
cov, cov_hits = coverage(item["text"], desc_tokens)
fams = family_hits(text, concepts)
if use_families:
predicted = (not hit_neg) and required_families <= set(fams) and len(fams) >= families_required
basis = f"fams={','.join(sorted(fams)) or '-'}"
else:
predicted = (not hit_neg) and cov >= threshold
basis = f"cov={cov:.2f}"
if expected is None:
verdict = "boundary"
entry_verdict = "边界条目(不计分)"
else:
ok_ = predicted == expected
n_total += 1
n_mismatch += 0 if ok_ else 1
verdict = "pass" if ok_ else "MISMATCH"
entry_verdict = ("一致" if ok_ else "不一致") + f"(预测={'触发' if predicted else '不触发'},期望={'触发' if expected else '不触发'})"
flag = "" if verdict in ("pass", "boundary") else " <-- 需修 description"
print(f"[{verdict:>9}] {item['text'][:48]} {basis} 负词命中={hit_neg} {entry_verdict}{flag}")
report["cases"].append({
"text": item["text"], "family": item["family"], "bucket": bucket,
"coverage": round(cov, 3), "coverage_hits": cov_hits,
"families_hit": sorted(fams),
"negative_hit": hit_neg, "predicted_trigger": predicted,
"expected_trigger": expected, "verdict": verdict,
})
report["summary"] = {"scored": n_total, "mismatch": n_mismatch,
"pass_rate": round((n_total - n_mismatch) / n_total, 3) if n_total else None}
out = args.output or (root / "reports" / "trigger-eval.json")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"\n计分条目 {n_total},不一致 {n_mismatch};报告已写入 {out}")
if n_mismatch:
print("结论:description 与用户原话存在词面缺口,先补触发短语/负触发再扩正文。")
return 1
print("结论:冒烟全绿。仍需真实对话实测后方可声称[已验证]。")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""LeMetaSkill 结构体检:检查一个 skill 目录是否满足基本契约。
只依赖标准库。检查项(FAIL 阻断 / WARN 提醒):
- SKILL.md 存在、frontmatter 可解析 [FAIL]
- name 存在、kebab-case、1-64 字符、与目录名一致 [FAIL]
- description 存在且够长(>=30 字符) [WARN]
- description 含负触发信号("不要用于"类表述) [WARN]
- 正文非空、<=500 行 [WARN]
- 正文中的本地 .md 链接可达(无悬空引用) [FAIL]
- evals/*.json 可解析 [FAIL]
- 无密钥/私有绝对路径 [WARN]
- 顶层目录都是已知用途 [WARN]
用法: python3 validate_skill.py <skill目录>
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
KNOWN_DIRS = {"references", "scripts", "evals", "assets", "reports", "examples", "tests"}
MAX_BODY_LINES = 500
SECRET_PATTERNS = [
r"AKIA[0-9A-Z]{16}",
r"sk-[A-Za-z0-9]{20,}",
r"(?:api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}",
r"ghp_[A-Za-z0-9]{36}",
]
PRIVATE_PATH = "/Users" + "/[A-Za-z0-9_.\\-]+/" # 拼接写法:避免扫描器匹配到自身源码
NEGATIVE_HINT = re.compile(r"不要|不该|不用于|排除|exclude|not for|don'?t use|do not use", re.I)
MD_LINK = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
def parse_frontmatter(text: str) -> tuple[dict[str, str], int]:
"""解析简易 YAML frontmatter(key: value 与 key: | 块),返回 (字段, 正文起始行号)。"""
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return {}, 0
fields: dict[str, str] = {}
key = ""
for i in range(1, min(len(lines), 60)):
line = lines[i]
if line.strip() == "---":
return fields, i + 1
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
if m:
key = m.group(1)
fields[key] = m.group(2).strip().strip("'\"")
elif line.startswith((" ", "\t")) and key:
fields[key] = (fields[key] + " " + line.strip().strip("'\"")).strip()
return {}, 0 # 没有闭合的 ---
def main() -> int:
ap = argparse.ArgumentParser(description="skill 结构体检")
ap.add_argument("skill_dir", type=Path)
args = ap.parse_args()
root: Path = args.skill_dir.resolve()
if not root.is_dir():
print(f"FAIL 目录不存在: {root}")
return 1
n_pass = n_warn = n_fail = 0
def ok(msg: str) -> None:
nonlocal n_pass
n_pass += 1
print(f"PASS {msg}")
def warn(msg: str) -> None:
nonlocal n_warn
n_warn += 1
print(f"WARN {msg}")
def fail(msg: str) -> None:
nonlocal n_fail
n_fail += 1
print(f"FAIL {msg}")
skill_md = root / "SKILL.md"
if not skill_md.is_file():
fail("SKILL.md 不存在")
print(f"\n--- Summary --- PASS:{n_pass} WARN:{n_warn} FAIL:{n_fail}")
return 1
ok("SKILL.md 存在")
text = skill_md.read_text(encoding="utf-8")
fields, body_start = parse_frontmatter(text)
if not fields:
fail("frontmatter 缺失或未闭合(需要 --- 包裹的头)")
else:
ok("frontmatter 可解析")
name = fields.get("name", "")
if not name:
fail("缺少 name 字段")
else:
if not re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", name):
fail(f"name 不是 kebab-case: {name!r}")
elif not 1 <= len(name) <= 64:
fail(f"name 长度超出 1-64: {len(name)}")
else:
ok(f"name 合法: {name}")
if root.name != name:
fail(f"name 与目录名不一致: {name!r} vs {root.name!r}")
else:
ok("name 与目录名一致")
desc = fields.get("description", "")
if not desc:
fail("缺少 description 字段(触发信号,必填)")
else:
if len(desc) < 30:
warn(f"description 太短({len(desc)} 字符),盖不住触发场景")
else:
ok(f"description 长度充分({len(desc)} 字符)")
if NEGATIVE_HINT.search(desc):
ok("description 含负触发信号")
else:
warn("description 缺负触发(\"不要用于…\"),边界请求会犹豫")
body = "\n".join(text.splitlines()[body_start:]).strip()
if not body:
fail("正文为空")
else:
n_lines = len(body.splitlines())
if n_lines > MAX_BODY_LINES:
warn(f"正文 {n_lines} 行,超过 {MAX_BODY_LINES} 行预算——考虑拆进 references/")
else:
ok(f"正文 {n_lines} 行,在预算内")
# 悬空引用
dangling = []
for target in MD_LINK.findall(body):
if target.startswith(("http://", "https://", "mailto:", "#")):
continue
if not (root / target).exists():
dangling.append(target)
if dangling:
fail(f"正文引用的本地文件不存在: {', '.join(sorted(set(dangling)))}")
elif MD_LINK.search(body):
ok("正文本地链接全部可达")
# evals JSON
evals_dir = root / "evals"
if evals_dir.is_dir():
for j in sorted(evals_dir.glob("*.json")):
try:
json.loads(j.read_text(encoding="utf-8"))
ok(f"{j.relative_to(root)} 可解析")
except json.JSONDecodeError as e:
fail(f"{j.relative_to(root)} JSON 解析失败: {e}")
# 密钥与私有路径(只读文本类文件)
for p in sorted(root.rglob("*")):
if not p.is_file() or p.suffix not in {".md", ".json", ".py", ".sh", ".txt", ".yaml", ".yml"}:
continue
try:
content = p.read_text(encoding="utf-8")
except (UnicodeDecodeError, PermissionError):
continue
rel = p.relative_to(root)
for pat in SECRET_PATTERNS:
if re.search(pat, content):
warn(f"{rel} 疑似包含密钥/令牌(模式 {pat})")
if rel.name != "SKILL.md" and re.search(PRIVATE_PATH, content):
warn(f"{rel} 含私有绝对路径(/Users/…)")
# 顶层结构
unknown = [d.name for d in root.iterdir() if d.is_dir() and d.name not in KNOWN_DIRS]
if unknown:
warn(f"未知顶层目录(确认其用途,或移除): {', '.join(unknown)}")
present = [d.name for d in root.iterdir() if d.is_dir() and d.name in KNOWN_DIRS]
if present:
ok(f"标准目录: {', '.join(sorted(present))}")
print(f"\n--- Summary --- PASS:{n_pass} WARN:{n_warn} FAIL:{n_fail}")
if n_fail:
print("结论:未过体检,先修 FAIL 再交付。")
return 1
if n_warn:
print("结论:可交付,WARN 项酌情处理。")
else:
print("结论:全绿。")
return 0
if __name__ == "__main__":
sys.exit(main())