返回 Skills 目录
dontbesilent2025/dbskill已通过检查

SKILL DETAIL

dbs-skill-maker

dontbesilent2025/dbskill/dbs-skill-maker

dbs-skill-maker 是一个用于制作单个 Skill 的工具,它通过需求分析将用户反复遇到的问题转化为可安装、可分级验证的 Skill。该工具强调从问题出发,保留用户选择,按需引入理论,直接生成文件,保持主入口轻量,用行为验证,失败驱动修改,外部动作单独授权,并鼓励用户参与需求分析。 该工具提供了一套完整的流程,包括形成问题契约、行为契约、选择机制、生成候选、分级验证候选,以及本地交付和可选的 GitHub 发布。它支持结构校验、行为冒烟测试、留出/回归测试和安装交付四个验证等级,确保 Skill 的质量和可用性。

安装量 · 224查看来源

Installation

npx skills add https://github.com/dontbesilent2025/dbskill --skill dbs-skill-maker

技能文件

SKILL.md

最近同步 · 2026年8月29日

agents/openai.yaml
interface:
  display_name: "dbs-skill-maker"
  short_description: "把反复问题制作成可安装、可分级验证的单个 Skill"
  default_prompt: "使用 $dbs-skill-maker 和我一起分析反复遇到的问题,并制作成可安装、验证等级清楚的单个 Skill。"
references/evaluation.md
# 行为评测与改进

当候选 Skill 已经存在,或者用户提供了失败输出和反馈时读取本文件。

## 行为契约先于样本

评测先固定目标任务、必须行为、关键失败和允许变化。测试只观察输出、工具动作、边界与停止条件,不规定隐藏推理过程。

## 验证等级

| 等级 | 必需证据 | 不能替代它的证据 |
| --- | --- | --- |
| 1.结构校验 | 静态校验器通过 | 文件看起来完整 |
| 2.行为冒烟 | 至少 1 个真实主任务的实际输出通过评分 | 正则匹配到某个标题 |
| 3.留出/回归 | 盲测的留出与回归样本通过,无关键回归 | 执行者看过预期答案的试跑 |
| 4.安装交付 | GitHub 来源安装成功,且文件内容与源目录逐项一致 | README 里已经写了安装命令 |

报告最高已完成等级,同时列出下一级尚缺的证据。

## 最小评估组合

| 样本类型 | 检查内容 |
| --- | --- |
| 正常正例 | 主要任务是否完成 |
| 边界样本 | 缺少关键输入、权限不足或风险升高时能否收窄、追问或停止 |
| 近邻反例 | 相似请求是否会误触发或越界处理 |
| 留出样本 | 修改能否迁移到未参与设计的任务 |
| 回归样本 | 旧版本已经通过的关键能力是否仍然保留 |

新 Skill 先使用 3~6 个样本。高风险或准备公开发布时扩大覆盖。

## 样本格式

将本地样本保存在目标 Skill 的 `evals/evals.json`,默认不发布:

```json
{
  "skill": "skill-name",
  "behavior_contract": {
    "must": ["可观察行为"],
    "must_not": ["关键失败"]
  },
  "cases": [
    {
      "id": "normal-01",
      "type": "normal",
      "prompt": "用户原始请求",
      "input_files": [],
      "expected_behavior": ["只交给评分者的验收标准"]
    }
  ]
}
```

每次执行把结果记录在 `evals/results/<candidate-id>.json`,至少包含基线、候选、环境、逐样本结果、回归和最高验证等级。候选标识必须能指向一个文件快照或 Git commit,不使用“最新版”一类无法恢复的标识。

执行者只能读取 `prompt` 和必要输入文件。`expected_behavior` 交给评分者,不能提前泄露给执行者。

## 运行基线或候选

记录:

- 实际输出和工具动作;
- 每条行为契约是否满足;
- 失败发生的具体步骤;
- 权限、网络、文件和依赖状态;
- 模型或环境差异;
- 时间和 token,环境可取得时再记录。

新 Skill 没有旧版本时,把首个候选作为起点,检查它是否达到行为契约。已有 Skill 修改时,使用相同任务和环境比较基线与候选。

## 失败归因

每个失败先确定一个主要来源:

| 来源 | 处理 |
| --- | --- |
| Skill 指令缺陷 | 修改决策条件、步骤、reference 或 script |
| 触发描述缺陷 | 调整 `description` 的用途和使用条件 |
| 输入缺口 | 增加必要输入或最小追问条件 |
| 样本缺陷 | 修订冲突、失真或泄露答案的测试 |
| 评分缺陷 | 改写与真实任务无关的断言 |
| 工具环境故障 | 修复权限、依赖或打包 |
| 执行波动 | 重跑并降低结论强度 |

## 最小修改假设

对准备修改的问题记录:

```text
观察到的失败 → 推测原因 → 修改位置 → 预期改善 → 可能回归
```

优先补清决策与停止条件、调整步骤传递、增加必要检查点、沉淀确定性脚本、删除冲突规则。最后才增加更长的规则清单。

## 保留候选

候选可以保留,需要同时满足:

- 原始失败得到改善;
- 留出样本支持迁移;
- 没有任务边界、安全、事实或权限方面的关键回归;
- 收益没有依赖答案泄露或单个案例补丁;
- 新增成本与收益相称。

证据不足时写“继续试验”。候选整体退化时恢复快照,并保留失败记录。
references/github-publishing.md
# GitHub 可选发布

只有用户明确要求发布、分享、创建 GitHub 仓库或生成 `npx skills add` 安装命令时读取本文件。

本地 Skill 已经完成时,可以提醒一次发布能力。用户未选择发布时,不创建 GitHub 配置、README、仓库、commit、tag 或 Release。

## 发布前需要确定

优先从上下文和当前 Git 状态取得:

- 使用现有仓库还是创建新仓库;
- 仓库所有者与名称;
- 公开或私有;
- 默认分支;
- 是否只提交当前 Skill;
- 是否需要版本 tag 或 GitHub Release。

只有缺少的信息会改变远端目标、公开范围或版本策略时才问 1 个问题。

## 授权边界

“帮我准备发布”只覆盖本地仓库准备和校验。“发布到 GitHub”“创建仓库并推送”等明确表达可以覆盖对应远端动作。授权不清楚时,在 push、tag 或 Release 前停止并说明将发生什么。

不得自动发布用户未审查的私密样本、测试答案、运行记录、密钥、内部路径和本机配置。

## 单 Skill 仓库结构

默认准备:

```text
repository/
├── README.md
└── skills/
    └── skill-name/
        ├── SKILL.md
        ├── agents/          存在时复制
        ├── references/      存在时复制
        ├── scripts/         存在时复制
        └── assets/          存在时复制
```

`evals/`、缓存、临时文件和本机隐藏文件默认排除。公开测试确有用户价值时,先脱敏并让用户确认具体文件。

可以使用:

```bash
python3 scripts/prepare_github_repo.py <skill-directory> <staging-directory> --owner <owner> --repo <repo>
```

脚本校验 owner 和 repo 格式,逐项比较打包前后文件,并从 `description`、`agents/openai.yaml` 和实际脚本生成 README 的用途、使用示例与依赖。它只准备本地 staging 目录,不创建远端仓库,也不 push。

## README 最小内容

- Skill 解决的问题;
- 适用条件和主要边界;
- 安装命令;
- 最短使用示例;
- 实际需要的工具或环境依赖;
- 隐私或权限说明,确有需要时加入。

单 Skill 仓库默认安装命令:

```bash
npx -y skills add <owner>/<repo> -g --all
```

## 发布门禁

发布前检查:

1. 运行 Skill 结构校验与脚本测试;
2. 检查 Git diff、未跟踪文件和暂存区;
3. 检查密钥、个人路径、私密材料和 `evals/`;
4. 只使用明确路径暂存,禁止 `git add .` 和 `git add -A`;
5. commit 信息指向实际用户收益;
6. push 前再次确认仓库、分支和提交范围;
7. tag 或 Release 只在用户要求时创建。

发现删除、覆盖、远端分叉、已有 tag、疑似密钥或额外暂存文件时停止,将异常合并说明。

## 安装验证

README 中存在命令只能证明说明已生成。发布后需要在隔离环境运行:

```bash
bash scripts/verify_npx_install.sh <owner>/<repo> <skill-name> <source-skill-directory>
```

验证至少包括:

- `npx skills add` 能发现并安装仓库中的 Skill;
- 安装目录存在 `SKILL.md`;
- frontmatter `name` 与预期一致;
- `SKILL.md`、agents、references、scripts 和 assets 与源目录逐文件一致。

网络、GitHub 权限或安装器故障时保留本地 Skill,报告失败发生在哪一步,不反复创建仓库或重复 push。
references/problem-and-goal.md
# 问题契约与完成条件

当用户只有想法、功能名称或模糊愿望时读取本文件。目标是确定新 Skill 反复解决的问题,以及什么证据能够证明它完成了任务。

## 任务判断

先提取下面的变化链:

```text
使用情境 → 当前卡住的进展 → 希望发生的变化 → Skill 交付物 → 完成证据
```

使用任务陈述:

> 当我处于 `{情境}`,我想让 Agent `{推进的变化}`,以便 `{得到的结果}`。

“推进的变化”要描述状态变化。“整理资料”“写提示词”“创建 Skill”通常只是候选手段,需要继续判断用户完成后能得到什么。

## 最小问题契约

```markdown
## 问题契约

反复出现的问题:
典型使用情境:
当前方案及其稳定失败:
用户想推进的变化:
Skill 需要交付的结果:
完成证据:
可接受代价:
不处理的近邻问题:
仍待验证的事实:
```

默认用户可以参与需求分析。对问题契约、完成证据和近邻边界存在的分歧,可以邀请用户判断,并说明该判断将影响哪个 Skill 设计。缺少材料时先给“待验证假设”,继续交付当前可用版本。合并相关问题,不询问从上下文已经得到答案的内容。

## 是否值得做成 Skill

至少检查以下特征:

| 特征 | 可沉淀信号 | 暂缓信号 |
| --- | --- | --- |
| 重复性 | 同类问题会再次出现 | 完全一次性的现场任务 |
| 稳定动作 | 可以提炼判断、转换或执行动作 | 每次都依赖无法归纳的临场决定 |
| 可观察结果 | 能看到文件、动作、判断或状态 | 只能描述抽象感受 |
| 输入可得 | 必要材料和权限能够取得 | 关键事实长期不可获得 |
| 边界可写 | 能识别适用与退出条件 | 希望处理所有相邻问题 |
| 复用收益 | 下次调用能减少判断或操作成本 | 制作成本持续高于重复收益 |

不满足全部特征时,可以收窄任务。保留能稳定复用的部分,避免为单个案例生成过窄规则。

## 行为契约

问题契约已经可用后,形成:

```markdown
## 行为契约

目标任务:
预期使用条件:
输入变化范围:

必须做到:
- ...

禁止出现:
- ...

允许变化:
- ...

关键失败:
- ...
```

“必须做到”只写可观察输出或动作。“允许变化”用于保留模型对开放任务的判断空间。“关键失败”优先覆盖任务错位、安全、事实、权限和近邻边界。

## 完成条件

问题阶段可以在下面条件满足时结束:

- 下一步动作能够确定;
- 候选 Skill 的交付物能够确定;
- 用户或测试者能够识别完成;
- 近邻边界足以避免明显误触发;
- 未知项不会改变当前候选的主要结构。
references/skill-construction.md
# Skill 构建规范

当问题契约、行为契约和关键机制已经足够清楚,需要创建或实质修改 Skill 文件时读取本文件。

## 选择最小结构

每个 Skill 必须有 `SKILL.md`。其他目录按实际用途添加:

- `agents/openai.yaml`:Codex UI 元数据或调用策略;
- `references/`:只在特定条件下读取的规则、标准、模式和案例;
- `scripts/`:重复、确定且需要可靠执行的操作;
- `assets/`:会复制或进入最终交付的模板、图片、字体和项目骨架。

不创建空目录、占位 README、更新日志和重复说明。GitHub 打包需要安装说明时再生成 README。

## 命名与发现

- 名称使用小写英文、数字和连字符,少于 64 个字符;
- 目录名与 frontmatter `name` 一致;
- `description` 同时说明能力和使用条件;
- `description` 遵守 1~1,024 个字符规范,建议尽量控制在 300 个字符内;
- 相邻任务容易误触发时,增加一个有信息量的边界;
- 默认允许正常自动发现,用户明确要求显式调用时再关闭隐式调用。

## `SKILL.md` 写什么

主入口保留:

- 任务和产出;
- 关键不变量;
- 状态或模式选择标准;
- 必要输入和最少提问条件;
- 核心工作方式;
- 授权、停止与安全边界;
- references 和 scripts 的读取或运行条件。

入口超过约 300 行时检查是否存在可以按条件移入 reference 的大段内容。这里是上下文预算提醒,没有平台行数硬限制。

避免写入模型已经具备的通用建议、无法验证的口号、单个案例答案和对所有任务都生效的细碎规则。

## 脚本判断

满足以下任一条件时考虑脚本:

- 同一转换会反复重写;
- 文件操作需要确定结果;
- API 调用、数据处理或校验需要稳定参数;
- 纯指令执行容易漏掉关键检查。

脚本必须验证成功路径和至少一个失败路径。涉及写入、网络、发布或删除时,保留清楚的授权和停止条件。

## 初始化与交付的边界

`init_skill_project.py` 需要任务、工作动作和完成证据,以便初始文件不含空占位符。这些参数只构成候选初稿,Agent 仍需要:

1. 根据问题契约补足适用条件、边界和停止条件;
2. 删除没有改变行为的通用句子;
3. 扫描整个目录的占位符、本机绝对路径和敏感值;
4. 完成静态校验和当前能够执行的行为测试。

只有结构校验时,交付报告必须写明“行为尚未验证”。

## UI 元数据

`agents/openai.yaml` 中:

- 字符串全部加引号;
- `display_name` 使用稳定名称;
- `short_description` 简短说明独有能力;
- `default_prompt` 使用 1 句话,并显式包含 `$skill-name`;
- 只有真实依赖 MCP 工具时才声明 `dependencies`;
- 未经用户明确要求,不设置 `allow_implicit_invocation: false`。

## 完成前检查

1. `description` 能区分当前任务和近邻任务;
2. 所有相对引用真实存在;
3. scripts 已运行验证;
4. 没有未完成占位符;
5. 用户的明确选择和授权边界得到保留;
6. 私密信息没有写进可分发文件;
7. 入口只加载当前任务需要的共同信息。

使用本 Skill 自带的校验器:

```bash
python3 scripts/validate_skill_project.py /absolute/path/to/skill
```
references/theory-and-mechanism.md
# 理论与机制设计

当问题已经清楚,但 Skill 的关键判断缺少可靠机制、适用条件或反证方式时读取本文件。

理论只服务于具体动作。一个理论、行业标准或经验规则进入 Skill,需要改变至少一项:判断顺序、分类标准、中间产物、适用边界或验证方法。

## 先拆判断动作

```markdown
| 判断动作 | 输入 | 需要得到的中间结果 | 缺少它会发生什么错误 |
| --- | --- | --- | --- |
```

常见动作包括:定义、归类、找因果、识别任务、分层、比较方案、找反例、生成方案、验证现实、决定停止。

## 机制候选

每个动作提出最少数量的候选机制:

```markdown
| 动作 | 候选理论/规则/工具 | 为什么适合 | 可能失效的条件 | 核实状态 |
| --- | --- | --- | --- | --- |
```

优先级:

1. 用户已经验证的现实规则或明确行业标准;
2. 能直接解释当前判断动作的可信理论;
3. 多个独立案例反复出现的条件性机制;
4. 可证伪、可替换的暂定假设。

名人辨识度和术语新鲜感不能单独构成入选理由。

## 核实要求

外部事实会改变 Skill 的判断时才进行外部研究。优先使用原著、原始论文、官方标准和权威机构材料。无法核实原始来源时标注权威二手来源或尚未核实,不猜测作者、年份、引文和页码。

研究简单问题时保留:

- 1 个主机制;
- 0~2 个补充机制;
- 至少 1 个纠偏方向或失败边界。

候选很多时,根据机制匹配、来源可靠、能否导出行为、边界清晰度和信息增量筛选。

## 映射到 Skill

```markdown
| Skill 步骤 | 使用机制 | 输入 | 输出 | 拦截的错误 | 失效边界 |
| --- | --- | --- | --- | --- | --- |
```

每个机制必须落到具体步骤。多个机制可以顺序执行、并行分析或分层检验,需要说明中间结果怎样传递。

## 小样本试跑

将候选机制用于一个真实片段,记录:

- 试跑输入;
- 每一步产生的中间判断;
- 比常识新增的判断;
- 能导出的行动;
- 能够推翻它的信号。

跑不出新判断、无法导出行动或无法设置失败信号的机制,从候选 Skill 中删除。理论研究结束后只把实际改变 Agent 行为的部分写入 Skill。
scripts/init_skill_project.py
#!/usr/bin/env python3
"""Create a minimal single-Skill project without overwriting existing files."""

from __future__ import annotations

import argparse
import json
import re
from pathlib import Path


NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
RESOURCE_NAMES = {"references", "scripts", "assets"}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="创建单个 Skill 的最小项目骨架")
    parser.add_argument("name", help="Skill 名称,小写英文、数字和连字符")
    parser.add_argument("--output", required=True, help="Skill 父目录")
    parser.add_argument("--description", required=True, help="能力与使用条件")
    parser.add_argument("--task", required=True, help="反复解决的问题与交付结果")
    parser.add_argument(
        "--workflow",
        action="append",
        required=True,
        help="一个关键工作动作,可重复传入",
    )
    parser.add_argument(
        "--done",
        action="append",
        required=True,
        help="一条可观察的完成证据,可重复传入",
    )
    parser.add_argument(
        "--resources",
        default="",
        help="按需创建 references,scripts,assets,使用英文逗号分隔",
    )
    parser.add_argument("--short-description", default="", help="Codex UI 短描述")
    parser.add_argument("--explicit-only", action="store_true", help="只允许显式调用")
    return parser.parse_args()


def yaml_string(value: str) -> str:
    return json.dumps(value, ensure_ascii=False)


def main() -> int:
    args = parse_args()
    name = args.name.strip()
    description = " ".join(args.description.split())
    task = " ".join(args.task.split())
    workflow = [" ".join(item.split()) for item in args.workflow if item.strip()]
    done = [" ".join(item.split()) for item in args.done if item.strip()]

    if not NAME_PATTERN.fullmatch(name) or len(name) >= 64:
        raise SystemExit("名称必须少于 64 个字符,并只使用小写英文、数字和连字符")
    if not 1 <= len(description) <= 1024:
        raise SystemExit("description 必须在 1~1,024 个字符之间")
    if not task:
        raise SystemExit("task 不能为空")
    if not workflow:
        raise SystemExit("至少需要一个 workflow")
    if not done:
        raise SystemExit("至少需要一条 done")

    resources = {item.strip() for item in args.resources.split(",") if item.strip()}
    unknown = sorted(resources - RESOURCE_NAMES)
    if unknown:
        raise SystemExit(f"未知资源目录:{', '.join(unknown)}")

    short_description = args.short_description.strip() or f"把明确问题制作成可验证的 {name} Skill"
    if not 25 <= len(short_description) <= 64:
        raise SystemExit("short-description 应在 25~64 个字符之间")

    target = Path(args.output).expanduser().resolve() / name
    if target.exists():
        raise SystemExit(f"目标已经存在,未覆盖:{target}")

    target.mkdir(parents=True)
    agents_dir = target / "agents"
    agents_dir.mkdir()
    for resource in sorted(resources):
        (target / resource).mkdir()

    workflow_lines = "\n".join(f"{index}. {item}" for index, item in enumerate(workflow, 1))
    done_lines = "\n".join(f"- {item}" for item in done)
    skill_text = f"""---
name: {name}
description: {description}
---

# {name}

## 任务

{task}

## 工作方式

{workflow_lines}

## 完成条件

{done_lines}
"""
    (target / "SKILL.md").write_text(skill_text, encoding="utf-8")

    openai_lines = [
        "interface:",
        f"  display_name: {yaml_string(name)}",
        f"  short_description: {yaml_string(short_description)}",
        f"  default_prompt: {yaml_string(f'使用 ${name} 完成它所解决的任务,并按可观察标准检查结果。')}",
    ]
    if args.explicit_only:
        openai_lines.extend(["policy:", "  allow_implicit_invocation: false"])
    (agents_dir / "openai.yaml").write_text("\n".join(openai_lines) + "\n", encoding="utf-8")

    print(f"已创建:{target}")
    print("下一步:补全适用条件、边界和停止条件,再运行 validate_skill_project.py。")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
scripts/prepare_github_repo.py
#!/usr/bin/env python3
"""Prepare a local staging directory for a single-Skill GitHub repository."""

from __future__ import annotations

import argparse
import filecmp
import re
import shutil
from pathlib import Path


FRONTMATTER_PATTERN = re.compile(r"\A---\s*\n(.*?)\n---(?:\s*\n|\Z)", re.DOTALL)
ALLOWED_ENTRIES = ("SKILL.md", "agents", "references", "scripts", "assets")
EXCLUDED_NAMES = {".DS_Store", "__pycache__", "evals"}
OWNER_PATTERN = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$")
REPO_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")


def read_scalar(frontmatter: str, field: str) -> str:
    lines = frontmatter.splitlines()
    prefix = f"{field}:"
    for index, line in enumerate(lines):
        if not line.startswith(prefix):
            continue
        value = line[len(prefix) :].strip()
        if value not in {"|", ">"}:
            return value.strip("\"'")
        content: list[str] = []
        for continuation in lines[index + 1 :]:
            if continuation and not continuation[0].isspace():
                break
            content.append(continuation[2:] if continuation.startswith("  ") else continuation)
        return ("\n" if value == "|" else " ").join(content).strip()
    return ""


def copy_public_tree(source: Path, destination: Path) -> None:
    if source.is_symlink():
        raise SystemExit(f"拒绝复制软链,请先确认真实来源:{source}")
    if source.is_file():
        if source.name not in EXCLUDED_NAMES and source.suffix != ".pyc":
            destination.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(source, destination)
        return

    destination.mkdir(parents=True, exist_ok=True)
    for child in sorted(source.iterdir()):
        if child.name in EXCLUDED_NAMES or child.suffix == ".pyc":
            continue
        copy_public_tree(child, destination / child.name)


def public_files(root: Path) -> dict[Path, Path]:
    files: dict[Path, Path] = {}
    for entry_name in ALLOWED_ENTRIES:
        entry = root / entry_name
        if not entry.exists():
            continue
        candidates = [entry] if entry.is_file() else sorted(entry.rglob("*"))
        for candidate in candidates:
            if candidate.is_symlink():
                raise SystemExit(f"拒绝打包软链接:{candidate}")
            if not candidate.is_file():
                continue
            if any(part in EXCLUDED_NAMES for part in candidate.relative_to(root).parts):
                continue
            if candidate.suffix == ".pyc":
                continue
            files[candidate.relative_to(root)] = candidate
    return files


def read_default_prompt(source: Path, name: str) -> str:
    openai_path = source / "agents" / "openai.yaml"
    if openai_path.is_file():
        match = re.search(r'^\s+default_prompt:\s*["\'](.*)["\']\s*$', openai_path.read_text(encoding="utf-8"), re.MULTILINE)
        if match:
            return match.group(1)
    return f"使用 ${name} 完成它所解决的任务。"


def detect_requirements(source: Path) -> list[str]:
    requirements = ["支持 Agent Skills 的客户端"]
    scripts = source / "scripts"
    if scripts.is_dir() and any(scripts.rglob("*.py")):
        requirements.append("Python 3(用于运行 Skill 内脚本)")
    if scripts.is_dir() and any(scripts.rglob("*.sh")):
        requirements.append("Bash(用于运行 Skill 内脚本)")
    openai_path = source / "agents" / "openai.yaml"
    if openai_path.is_file():
        text = openai_path.read_text(encoding="utf-8")
        for value in re.findall(r'^\s+value:\s*["\']?([^"\'\s]+)', text, re.MULTILINE):
            requirements.append(f"工具依赖:{value}")
    return requirements


def main() -> int:
    parser = argparse.ArgumentParser(description="准备单 Skill GitHub 仓库的本地 staging 目录")
    parser.add_argument("skill_directory")
    parser.add_argument("staging_directory")
    parser.add_argument("--owner", required=True)
    parser.add_argument("--repo", required=True)
    args = parser.parse_args()

    source = Path(args.skill_directory).expanduser().resolve()
    staging = Path(args.staging_directory).expanduser().resolve()
    owner = args.owner.strip()
    repo = args.repo.strip()
    if not source.is_dir():
        raise SystemExit(f"源 Skill 目录不存在:{source}")
    if not OWNER_PATTERN.fullmatch(owner):
        raise SystemExit("GitHub owner 格式无效")
    if not REPO_PATTERN.fullmatch(repo) or repo in {".", ".."}:
        raise SystemExit("GitHub repo 格式无效")
    skill_path = source / "SKILL.md"
    if not skill_path.is_file():
        raise SystemExit(f"源目录缺少 SKILL.md:{source}")
    if staging.exists():
        if not staging.is_dir():
            raise SystemExit(f"staging 路径已存在且不是目录:{staging}")
        if any(staging.iterdir()):
            raise SystemExit(f"staging 目录非空,未覆盖:{staging}")

    text = skill_path.read_text(encoding="utf-8")
    match = FRONTMATTER_PATTERN.match(text)
    if match is None:
        raise SystemExit("SKILL.md 缺少合法 frontmatter")
    name = read_scalar(match.group(1), "name")
    description = " ".join(read_scalar(match.group(1), "description").split())
    if not name or not description:
        raise SystemExit("SKILL.md 缺少 name 或 description")

    target_skill = staging / "skills" / name
    source_files = public_files(source)
    staging.mkdir(parents=True, exist_ok=True)
    for entry_name in ALLOWED_ENTRIES:
        entry = source / entry_name
        if entry.exists():
            copy_public_tree(entry, target_skill / entry_name)

    copied_files = public_files(target_skill)
    if set(source_files) != set(copied_files):
        missing = sorted(str(path) for path in set(source_files) - set(copied_files))
        extra = sorted(str(path) for path in set(copied_files) - set(source_files))
        raise SystemExit(f"打包资源清单不一致;缺少:{missing};多出:{extra}")
    for relative, source_file in source_files.items():
        if not filecmp.cmp(source_file, copied_files[relative], shallow=False):
            raise SystemExit(f"打包后文件内容不一致:{relative}")

    locator = f"{owner}/{repo}"
    usage = read_default_prompt(source, name)
    requirement_lines = "\n".join(f"- {item}" for item in detect_requirements(source))
    readme = f"""# {name}

{description}

## 安装

```bash
npx -y skills add {locator} -g --all
```

## 使用

安装后,可以这样开始:

> {usage}

## 运行依赖

{requirement_lines}

## 内容

本仓库只发布运行这个 Skill 所需的文件。本地评测样本、预期答案和运行记录不包含在公开包中。
"""
    (staging / "README.md").write_text(readme, encoding="utf-8")
    (staging / ".gitignore").write_text(
        ".DS_Store\n__pycache__/\n*.pyc\nevals/\n",
        encoding="utf-8",
    )

    print(f"已准备:{staging}")
    print(f"安装命令:npx -y skills add {locator} -g --all")
    print("尚未创建远端仓库,也没有执行 git push。")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
scripts/validate_skill_project.py
#!/usr/bin/env python3
"""Validate observable structure and references for one Skill directory."""

from __future__ import annotations

import argparse
import re
import subprocess
from pathlib import Path


NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
FRONTMATTER_PATTERN = re.compile(r"\A---\s*\n(.*?)\n---(?:\s*\n|\Z)", re.DOTALL)
LINK_PATTERN = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
TEXT_SUFFIXES = {".md", ".json", ".yaml", ".yml", ".py", ".sh", ".txt"}
UNFINISHED_MARKERS = ("[TODO", "TODO:", "FIXME", "{说明", "{写入", "{可观察", "<待填写>")
LOCAL_PATH_PATTERN = re.compile(r"(?:/Users/[^/\s]+/|/home/[^/\s]+/|[A-Za-z]:\\\\Users\\\\[^\\\s]+\\\\)")
SECRET_PATTERN = re.compile(
    r"(?i)(?:api[_-]?key|access[_-]?token|secret|password)\s*[:=]\s*['\"]?([A-Za-z0-9_\-]{16,})"
)
ALLOWED_ROOT_ENTRIES = {"SKILL.md", "agents", "references", "scripts", "assets", "evals"}


def read_scalar(frontmatter: str, field: str) -> str:
    lines = frontmatter.splitlines()
    prefix = f"{field}:"
    for index, line in enumerate(lines):
        if not line.startswith(prefix):
            continue
        value = line[len(prefix) :].strip()
        if value not in {"|", ">"}:
            return value.strip("\"'")
        content: list[str] = []
        for continuation in lines[index + 1 :]:
            if continuation and not continuation[0].isspace():
                break
            content.append(continuation[2:] if continuation.startswith("  ") else continuation)
        return ("\n" if value == "|" else " ").join(content).strip()
    return ""


def read_text(path: Path, errors: list[str]) -> str | None:
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        errors.append(f"文本文件不是 UTF-8:{path}")
        return None


def check_markdown_links(root: Path, path: Path, text: str, errors: list[str]) -> None:
    for link in LINK_PATTERN.findall(text):
        target = link.split("#", 1)[0].strip().strip("<>")
        if not target or target.startswith(("http://", "https://", "mailto:", "#")):
            continue
        if target.startswith("/"):
            errors.append(f"引用使用了绝对路径:{path.relative_to(root)} -> {target}")
            continue
        resolved = (path.parent / target).resolve()
        if root not in resolved.parents and resolved != root:
            errors.append(f"引用越出 Skill 目录:{path.relative_to(root)} -> {target}")
        elif not resolved.exists():
            errors.append(f"引用不存在:{path.relative_to(root)} -> {target}")


def main() -> int:
    parser = argparse.ArgumentParser(description="校验单个 Skill 项目")
    parser.add_argument("skill_directory")
    args = parser.parse_args()

    root = Path(args.skill_directory).expanduser().resolve()
    errors: list[str] = []
    warnings: list[str] = []
    skill_path = root / "SKILL.md"

    if not root.is_dir():
        print(f"ERROR:目录不存在:{root}")
        return 1
    if not skill_path.is_file():
        print(f"ERROR:缺少 {skill_path}")
        return 1

    text = skill_path.read_text(encoding="utf-8")
    match = FRONTMATTER_PATTERN.match(text)
    if match is None:
        errors.append("SKILL.md 缺少合法的 YAML frontmatter")
        frontmatter = ""
    else:
        frontmatter = match.group(1)

    name = read_scalar(frontmatter, "name")
    description = read_scalar(frontmatter, "description")
    if not NAME_PATTERN.fullmatch(name) or len(name) >= 64:
        errors.append("name 必须少于 64 个字符,并只使用小写英文、数字和连字符")
    if name and name != root.name:
        errors.append(f"目录名 {root.name!r} 与 name {name!r} 不一致")
    if not 1 <= len(description) <= 1024:
        errors.append(f"description 长度为 {len(description)},规范范围是 1~1,024")
    elif len(description) > 300:
        warnings.append(f"description 长度为 {len(description)},建议检查是否可以收紧")
    if description and not any(token in description.lower() for token in ("时使用", "用户", "当", "when", "use for", "used for")):
        warnings.append("description 可能只说明了能力,建议补充使用条件")

    for path in sorted(root.rglob("*")):
        relative = path.relative_to(root)
        if path.is_symlink():
            errors.append(f"不允许悬空或外部软链接:{relative}")
            continue
        if path.is_dir():
            if not any(path.iterdir()):
                warnings.append(f"空目录没有产生功能:{relative}")
            continue
        if path.suffix.lower() not in TEXT_SUFFIXES:
            continue
        file_text = read_text(path, errors)
        if file_text is None:
            continue
        is_validator_source = path.resolve() == Path(__file__).resolve()
        if not is_validator_source:
            for marker in UNFINISHED_MARKERS:
                if marker in file_text:
                    errors.append(f"仍有未完成占位符:{relative} -> {marker}")
            if LOCAL_PATH_PATTERN.search(file_text):
                errors.append(f"包含本机用户绝对路径:{relative}")
            if SECRET_PATTERN.search(file_text):
                errors.append(f"包含疑似密钥或密码:{relative}")
        if path.suffix.lower() == ".md":
            check_markdown_links(root, path, file_text, errors)

    for entry in sorted(root.iterdir()):
        if entry.name not in ALLOWED_ROOT_ENTRIES:
            warnings.append(f"Skill 根目录存在非标准条目:{entry.name}")

    line_count = len(text.splitlines())
    if line_count > 300:
        warnings.append(f"SKILL.md 共 {line_count} 行,建议检查渐进式披露")

    openai_path = root / "agents" / "openai.yaml"
    if openai_path.is_file():
        openai_text = openai_path.read_text(encoding="utf-8")
        if name and f"${name}" not in openai_text:
            errors.append("agents/openai.yaml 的 default_prompt 未显式包含 Skill 名")
        short_match = re.search(r'^\s+short_description:\s*["\'](.*)["\']\s*$', openai_text, re.MULTILINE)
        if short_match and not 25 <= len(short_match.group(1)) <= 64:
            errors.append("agents/openai.yaml 的 short_description 应为 25~64 个字符")
        display_match = re.search(r'^\s+display_name:\s*["\'](.*)["\']\s*$', openai_text, re.MULTILINE)
        if display_match and name and display_match.group(1) != name:
            errors.append("agents/openai.yaml 的 display_name 与 frontmatter name 不一致")

    scripts_dir = root / "scripts"
    if scripts_dir.is_dir():
        for script in sorted(scripts_dir.rglob("*.py")):
            try:
                compile(script.read_text(encoding="utf-8"), str(script), "exec")
            except SyntaxError as error:
                errors.append(f"Python 语法错误:{script.relative_to(root)}:{error}")
        for script in sorted(scripts_dir.rglob("*.sh")):
            result = subprocess.run(
                ["bash", "-n", str(script)],
                capture_output=True,
                text=True,
                check=False,
            )
            if result.returncode:
                detail = result.stderr.strip() or "bash -n 失败"
                errors.append(f"Shell 语法错误:{script.relative_to(root)}:{detail}")

    for warning in warnings:
        print(f"WARNING:{warning}")
    for error in errors:
        print(f"ERROR:{error}")

    if errors:
        print(f"校验失败:{len(errors)} 个错误,{len(warnings)} 个提醒。")
        return 1
    print(f"校验通过:{root}({len(warnings)} 个提醒)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
scripts/verify_npx_install.sh
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 3 ]]; then
  echo "用法:$0 <owner/repo-or-local-path> <skill-name> <source-skill-directory>" >&2
  exit 2
fi

source_locator="$1"
skill_name="$2"
source_skill_directory="$3"
skill_maker_npx="${SKILL_MAKER_NPX:-npx}"
temporary_root="$(mktemp -d)"
temporary_home="$temporary_root/home"

if [[ ! "$skill_name" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
  echo "安装验证失败:Skill 名称格式无效" >&2
  exit 2
fi

if [[ ! -f "$source_skill_directory/SKILL.md" ]]; then
  echo "安装验证失败:源目录缺少 SKILL.md" >&2
  exit 2
fi

if find "$source_skill_directory" -type l -print -quit | grep -q .; then
  echo "安装验证失败:源 Skill 包含软链接" >&2
  exit 2
fi

cleanup() {
  rm -rf "$temporary_root"
}
trap cleanup EXIT

mkdir -p "$temporary_home"

HOME="$temporary_home" "$skill_maker_npx" -y skills add "$source_locator" -g --all

installed_skill=""
while IFS= read -r candidate; do
  installed_skill="$candidate"
  break
done < <(find "$temporary_home" -type f -path "*/skills/$skill_name/SKILL.md" -print)

if [[ -z "$installed_skill" ]]; then
  echo "安装验证失败:未找到 $skill_name/SKILL.md" >&2
  exit 1
fi

if ! grep -Eq "^name:[[:space:]]*['\"]?$skill_name['\"]?[[:space:]]*$" "$installed_skill"; then
  echo "安装验证失败:frontmatter name 与 $skill_name 不一致" >&2
  exit 1
fi

installed_root="$(dirname "$installed_skill")"
checked_count=0
for entry in SKILL.md agents references scripts assets; do
  source_entry="$source_skill_directory/$entry"
  [[ -e "$source_entry" ]] || continue
  if [[ -f "$source_entry" ]]; then
    source_files=("$source_entry")
  else
    source_files=()
    while IFS= read -r source_file; do
      source_files+=("$source_file")
    done < <(find "$source_entry" -type f ! -name '.DS_Store' ! -name '*.pyc' -print)
  fi
  for source_file in "${source_files[@]}"; do
    relative_path="${source_file#"$source_skill_directory/"}"
    installed_file="$installed_root/$relative_path"
    if [[ ! -f "$installed_file" ]]; then
      echo "安装验证失败:缺少资源 $relative_path" >&2
      exit 1
    fi
    if ! cmp -s "$source_file" "$installed_file"; then
      echo "安装验证失败:资源内容不一致 $relative_path" >&2
      exit 1
    fi
    checked_count=$((checked_count + 1))
  done
done

echo "安装验证通过:${installed_skill}(已核对 ${checked_count} 个文件)"
SKILL.md
---
name: dbs-skill-maker
description: 通过需求分析把反复出现的问题制作成可安装、可分级验证的单个 Skill。用户要求做 Skill、把问题沉淀成 Skill、检查或测试候选 Skill 时使用;只有用户明确要求分享或发布时,才准备 GitHub 仓库并验证 `npx skills add`。
---

# dbs-skill-maker:单个 Skill 制作器

把用户反复遇到的一个问题,制作成经过验证、可以本地安装的单个 Skill。用户明确希望分享时,再提供 GitHub 发布和 `npx skills add` 安装能力。

## 核心任务

用户雇用本 Skill,是为了完成下面的进展:

> 将一类反复出现的问题,转化成一个能被 Agent 正确发现、稳定处理并接受验证的 Skill。

本地可用并通过当前证据支持的测试,即构成一次完整交付。GitHub 发布是可选能力,不能成为默认步骤。

## 不变量

1. **从问题开始。** 先确定 Skill 反复解决什么问题、在什么情境使用、出现什么证据算完成。
2. **保留用户选择。** 用户已经给出的名称、目标、平台和边界直接沿用;低风险细节可以透明假设。
3. **理论按需进入。** 只有理论能够改变判断步骤、适用边界或验证方式时才研究,不为装饰添加人物和术语。
4. **直接生成文件。** 用户要求制作时,交付真实 Skill 目录;材料足够时不把方案说明冒充成品。
5. **主入口保持轻。** `SKILL.md` 保存共同决策、关键流程和停止条件;条件性细节放进 `references/`,重复且确定的工作放进 `scripts/`。
6. **用行为验证。** 测试可观察输出、工具动作和边界,不要求模型展示隐藏推理。
7. **失败驱动修改。** 修改必须能够解释一类稳定失败,并检查留出样本和关键回归。
8. **外部动作单独授权。** 创建远端仓库、push、tag、Release 和公开测试材料,都需要用户明确要求。
9. **用户参与需求分析。** 默认用户具备基础专业能力。可以使用问题契约、行为契约、边界样本等概念,但要结合当前任务解释它们会改变哪个设计决定。

## 判断当前状态

每次先读取当前对话、用户提供的文件和目标目录,只处理当前最需要推进的状态。不要强迫用户重新走已经完成的阶段。

| 当前证据 | 当前任务 | 读取 |
| --- | --- | --- |
| 只有一个想法,问题和完成态不清 | 形成问题契约与完成条件 | [references/problem-and-goal.md](references/problem-and-goal.md) |
| 问题已清楚,关键判断缺少机制支撑 | 研究并筛选可用机制 | [references/theory-and-mechanism.md](references/theory-and-mechanism.md) |
| 问题、行为和边界已经清楚 | 生成或补全 Skill 文件 | [references/skill-construction.md](references/skill-construction.md) |
| 已有候选 Skill,尚无行为证据 | 建立样本并测试候选 | [references/evaluation.md](references/evaluation.md) |
| 已有失败输出或用户反馈 | 归因、提出最小修改并回归 | [references/evaluation.md](references/evaluation.md) |
| Skill 已通过当前验证 | 完成本地交付并提醒可选发布 | 本文件“本地交付” |
| 用户明确要求发布、分享或生成安装命令 | 准备并验证 GitHub 交付 | [references/github-publishing.md](references/github-publishing.md) |

同一轮可以使用上游已经形成的产物继续完成当前任务。不要把表格中的状态改写成固定问卷或要求用户逐项选择。

## 输入与提问

优先从当前上下文取得:

- 用户反复遇到的问题;
- 使用这个 Skill 的典型情境;
- 希望 Agent 产生的结果或动作;
- 已有案例、失败输出、文件和约束;
- 目标平台或目录;
- 当前是否只需本地使用。

信息足以决定交付物时直接推进。需求分析本身对 Skill 质量有价值,可以让用户参与核心判断;只有已经能从上下文可靠恢复的信息才不重复追问。将相关缺口合并成尽可能少的问题,说明每个问题将改变哪个设计决定,不发送无关的通用问卷。

## 从问题到候选 Skill

### 1.形成问题契约

至少明确:

```markdown
反复出现的问题:
使用情境:
用户想推进的变化:
Skill 应交付的结果:
完成证据:
不处理的近邻问题:
```

问题太一次性、成功无法观察、主要结果依赖不可获得的权限或事实时,说明当前不适合沉淀。仍可交付能够复用的局部工具。

### 2.形成行为契约

把“好用、聪明、专业”等要求改写成可观察行为:

```markdown
目标任务:
预期使用条件:
输入变化范围:
必须做到:
禁止出现:
允许变化:
关键失败:
```

### 3.选择机制

先拆出 Skill 需要完成的判断动作,再决定是否需要理论、行业规则、用户材料或脚本。每个机制都要对应一个动作、一个中间结果和一个失效边界。

### 4.生成候选

根据实际任务选择最小结构:

```text
skill-name/
├── SKILL.md
├── agents/openai.yaml       可选 UI 元数据
├── references/              条件性知识或流程
├── scripts/                 重复、确定的操作
└── assets/                  进入最终交付的模板或素材
```

目录只创建实际需要的部分。新 Skill 默认允许正常自动发现;只有用户明确要求显式调用时,才关闭隐式调用。

创建新目录时可以运行:

```bash
python3 scripts/init_skill_project.py <skill-name> --output <父目录> \
  --description "<能力与使用条件>" \
  --task "<反复解决的问题与交付结果>" \
  --workflow "<第 1 个关键动作>" \
  --done "<可观察的完成证据>"
```

脚本只负责生成已带真实内容的初稿。Agent 必须继续写完判断条件、边界和停止条件,并在校验通过后交付。不得把初始骨架或未完成占位符交给用户。

完成文件后运行:

```bash
python3 scripts/validate_skill_project.py <skill-directory>
```

### 5.分级验证候选

新 Skill 先建立 3~6 个样本,至少覆盖正常正例、边界样本、近邻反例和留出样本。复杂或高风险任务再扩大覆盖。执行者不得提前读取预期答案。

按实际完成的最高等级报告:

| 等级 | 证据 | 允许的表述 |
| --- | --- | --- |
| 1.结构校验 | frontmatter、引用、资源和脚本静态检查通过 | “结构校验通过” |
| 2.行为冒烟 | 候选 Skill 完成至少 1 个真实主要任务 | “行为冒烟测试通过” |
| 3.留出/回归 | 执行者未看预期答案,留出样本通过且无关键回归 | “当前留出与回归样本通过” |
| 4.安装交付 | GitHub 来源可用 `npx skills add` 安装,并逐项核对资源 | “GitHub `npx` 安装与资源校验通过” |

未实际运行行为任务时,只能报告第 1 级。没有留出收益或出现关键回归时,不能宣称 Skill 已经变强。详细记录方式见 [references/evaluation.md](references/evaluation.md)。

## 本地交付

完成后报告:

- Skill 名称和绝对路径;
- 解决的问题与主要边界;
- 实际生成的文件;
- 已运行的校验和行为测试;
- 仍未验证的风险;
- 本地安装或调用方式。

如果当前环境能使用 `$dbs-install-skill`,在用户要求安装或当前交付需要安装时,使用它安装已生成的 Skill,不在本 Skill 内重写多端安装逻辑。如果检测不到 dbskill 或 `$dbs-install-skill`,先说明使用 dbskill 可以完成多端安装和去重,并引导用户运行:

```bash
npx -y skills add dontbesilent2025/dbskill -g --all
```

安装 dbskill 后再继续安装新 Skill;不因缺少 dbskill 而临时生成另一套安装脚本。

本地交付后固定提醒一次:

> Skill 已完成,当前最高验证等级是「{1/2/3/4}」,可以在本地使用。如果你希望分享给别人,我还可以帮你发布到 GitHub,并验证 `npx skills add` 安装命令。

用户没有要求发布时直接结束,不继续追问,也不创建 GitHub 配置。

## GitHub 发布边界

用户明确要求发布、分享或生成 `npx` 安装命令时,才读取 [references/github-publishing.md](references/github-publishing.md)。

准备仓库不等于获得远端写入授权。执行 `gh repo create`、`git push`、创建 tag 或 Release 前,确认用户的明确要求能够覆盖该动作。

对单 Skill 仓库,默认生成的安装命令为:

```bash
npx -y skills add <owner>/<repo> -g --all
```

命令进入 README 后仍需在隔离目录中验证,不能只检查文字是否存在。

## 停止条件

出现以下情况时结束当前轮次:

- 候选已经完成并通过当前可执行的验证;
- 当前阶段缺少一个会改变设计的用户决定;
- 理论或现实事实连续核实失败,继续搜索不会改变候选;
- 修改开始依赖单个案例补丁;
- 用户只要求方案或明确要求停止;
- GitHub 相关动作尚未获得授权。

## 语言与安全

- 中文遵循《中文文案排版指北》。
- 不使用空洞的二元反转句式。
- 不读取、复制或发布用户未授权的私密材料。
- 不覆盖现有真实目录;发现同名目标时先检查并说明。
- 不使用 `git add .` 或 `git add -A`;发布时只暂存明确文件。
- 不把本地绝对路径、测试答案、密钥或内部记录写进公开仓库。

完成当前任务后直接结束。只有用户明确询问下一步,且当前环境已经安装 `/dbs` 时,简短提示:「下一步不确定时,可以输入 `/dbs`。」