add README guidance to skill workflow

This commit is contained in:
2026-07-29 21:08:04 +08:00
parent 262c138bee
commit e667f58bf1
9 changed files with 380 additions and 7 deletions
+3 -3
View File
@@ -176,8 +176,8 @@ skiff 不保存 token。可以使用 `company/code-review`,也可以使用
| 命令 | 说明 | | 命令 | 说明 |
|------|------| |------|------|
| `skiff create <name> [--idea TEXT] [--from-project PATH]` | 从模板创建草稿到 `~/.skills/.drafts/` | | `skiff create <name> [--idea TEXT] [--from-project PATH]` | 从模板创建`SKILL.md``README.md` 的草稿 |
| `skiff check <name>` | 校验草稿或正式 skill | | `skiff check <name>` | 校验草稿或正式 skill,包括人类使用说明 |
| `skiff finalize <name>` | 校验草稿并移动到正式 `skills/` | | `skiff finalize <name>` | 校验草稿并移动到正式 `skills/` |
| `skiff doctor [--target all] [--fix]` | 检查软链健康状态,`--fix` 自动修复 | | `skiff doctor [--target all] [--fix]` | 检查软链健康状态,`--fix` 自动修复 |
@@ -187,7 +187,7 @@ skiff 不保存 token。可以使用 `company/code-review`,也可以使用
```bash ```bash
skiff create my-skill --idea "描述要解决的重复问题" --from-project . skiff create my-skill --idea "描述要解决的重复问题" --from-project .
# 由 Agent 完善 ~/.skills/.drafts/my-skill/SKILL.md # 由 Agent 完善草稿中的 SKILL.md 和 README.md
skiff check my-skill skiff check my-skill
skiff finalize my-skill skiff finalize my-skill
skiff publish skills/my-skill -m "add my-skill" --push skiff publish skills/my-skill -m "add my-skill" --push
+10
View File
@@ -1001,6 +1001,16 @@ def cmd_create(args: argparse.Namespace) -> None:
flags=re.MULTILINE, flags=re.MULTILINE,
) )
skill_md.write_text(content, encoding="utf-8") skill_md.write_text(content, encoding="utf-8")
readme = dst / "README.md"
if readme.is_file():
readme_content = re.sub(
r"^#\s+skill-name\s*$",
f"# {args.name}",
readme.read_text(encoding="utf-8"),
count=1,
flags=re.MULTILINE,
)
readme.write_text(readme_content, encoding="utf-8")
brief = { brief = {
"name": args.name, "name": args.name,
"idea": args.idea or "", "idea": args.idea or "",
+16
View File
@@ -196,8 +196,24 @@ def validate_skill_dir(skill_dir: Path, expected_name: str) -> list[str]:
"""返回 skill 目录中的校验问题;空列表表示通过。""" """返回 skill 目录中的校验问题;空列表表示通过。"""
issues: list[str] = [] issues: list[str] = []
skill_md = skill_dir / "SKILL.md" skill_md = skill_dir / "SKILL.md"
readme = skill_dir / "README.md"
if not skill_md.is_file(): if not skill_md.is_file():
return ["缺少 SKILL.md"] return ["缺少 SKILL.md"]
if not readme.is_file():
issues.append("缺少 README.md(面向人类的使用说明)")
else:
readme_text = readme.read_text(encoding="utf-8")
if not readme_text.strip():
issues.append("README.md 为空")
if re.search(r"^#\s+skill-name\s*$", readme_text, re.MULTILINE):
issues.append("README.md 存在模板占位内容: skill-name")
for placeholder in (
"用一句话告诉使用者这个 skill 能解决什么问题",
"给出用户可以直接说出的典型请求",
"给出一条可以直接交给 Agent 的示例请求",
):
if placeholder in readme_text:
issues.append(f"README.md 存在模板占位内容: {placeholder}")
text = skill_md.read_text(encoding="utf-8") text = skill_md.read_text(encoding="utf-8")
if not text.strip(): if not text.strip():
+28
View File
@@ -0,0 +1,28 @@
# skill-name
用一句话告诉使用者这个 skill 能解决什么问题。
## 什么时候使用
- 给出用户可以直接说出的典型请求。
- 说明适用范围。
## 使用前准备
- 列出需要用户提前提供或确认的信息、权限和依赖。
## 使用示例
```text
给出一条可以直接交给 Agent 的示例请求。
```
## Agent 会做什么
1. 用面向使用者的语言概括主要步骤。
2. 说明可能发生的外部写操作或重要边界。
## 如何判断完成
- 说明使用者最终会得到什么,以及如何确认结果正确。
+67
View File
@@ -0,0 +1,67 @@
# deb-publisher
帮助 Agent 复用项目已有的 DEB 打包与发布方式,完成构建、包检查、上传和发布验证。
## 什么时候使用
当你希望 Agent 处理以下任务时使用:
- “帮我构建这个项目的 DEB 包”
- “把 1.2.3 版本的 DEB 发布到包仓库”
- “看看项目里的 DEB 发布流程”
- “检查这个 DEB 是否可以发布”
只想在本机安装一个 `.deb`,或者要构建 RPM、Docker 镜像时,不需要使用这个 skill。
## 使用前准备
请准备或确认:
- 要构建或发布的版本号。
- 项目已经配置好构建脚本、Make 目标或 `debian/` 目录。
- 发布所需的令牌已经放入项目约定的环境变量或密钥系统。
- 如果要真实发布,明确告诉 Agent 目标仓库以及是否允许覆盖同版本。
不要把令牌直接粘贴到对话、命令参数或项目文件中。
## 使用示例
仅分析,不产生或上传包:
```text
看看这个项目的 DEB 是怎么构建和发布的。
```
只构建和检查,不上传:
```text
使用 deb-publisher 构建 1.2.3 的 DEB,检查包元数据和内容,不要上传。
```
构建并发布:
```text
使用 deb-publisher 构建并发布 1.2.3 的 amd64 DEB 到项目已配置的测试仓库。
```
发布已有产物:
```text
使用 deb-publisher 检查并发布 ./dist/example_1.2.3_amd64.deb。
```
## Agent 会做什么
Agent 会优先发现和复用项目已有入口,然后:
1. 确认版本、架构、产物路径、目标仓库和授权范围。
2. 构建 DEB,或定位你指定的已有产物。
3. 检查包的元数据、内容和 SHA-256。
4. 在你明确要求发布时,通过项目已有脚本上传。
5. 检查服务端响应,并在仓库支持时确认该版本已经可见。
## 如何判断完成
结果中应包含包名、版本、架构、产物路径、SHA-256,以及构建、上传和仓库可见性的
独立状态。异步索引尚未完成时,Agent 应明确说明“上传已接受,索引待更新”。
+143
View File
@@ -0,0 +1,143 @@
---
name: deb-publisher
description: >-
构建并发布 Debian DEB 包:发现项目已有的 Makefile、打包脚本和仓库上传入口,
校验包元数据与内容,使用环境变量中的凭据上传,并验证发布结果。触发词:构建 deb、
发布 deb、上传 deb、提交 deb、推送 apt 仓库、打 Debian 包。仅分析打包逻辑时也可使用,
但不会在未获授权时执行上传。
---
# DEB Publisher
复用项目已有发布约定,安全地完成“发现入口 → 构建 → 检查 → 上传 → 验证”。
## 何时使用
- 用户要求构建、发布、上传或提交 `.deb` 包。
- 用户要求梳理或接通项目现有的 DEB 发布流程。
- 用户要求把已经生成的 `.deb` 推送到 APT/DEB 包仓库。
## 不适用
- 只需要安装或卸载本地 DEB 包。
- 目标是 RPM、APK、容器镜像或语言包管理器。
- 用户只要求设计全新的 Debian 打包体系;此时应先完成方案设计。
## 工作流
### 1. 发现项目约定
从项目根目录查找,不预设文件位置:
```bash
rg -n -i --hidden --glob '!.git' \
'build-deb|upload-deb|publish-deb|dpkg-deb|debuild|curl.*deb|\.deb\b|aptly|reprepro'
```
重点检查:
- `Makefile`、CI 配置、`debian/``scripts/` 和发布文档。
- 版本号、包名、架构、产物目录和仓库名如何传入。
- 发布端点、认证方式以及发布是否由构建目标自动触发。
- 当前工作树和目标版本是否匹配。
优先复用已有入口。除非用户要求改造,否则不要另建一套并行发布脚本。
### 2. 确认发布边界
上传是外部写操作。仅当用户明确要求发布、上传或提交时执行;若用户只要求查看、
诊断或构建,则停在相应阶段。
执行上传前确认:
- 目标服务和仓库来自项目配置或用户输入,不猜测生产端点。
- 认证令牌已通过环境变量或密钥系统提供。
- 目标版本、架构和产物路径能够从构建配置推导。
- 相同版本是否允许覆盖;无法确认且可能覆盖时,先询问用户。
绝不把令牌写入命令输出、文件、提交或最终回复。不要用 `set -x` 执行含凭据的脚本。
### 3. 构建包
使用项目声明的构建目标,并显式传入版本。例如项目提供 Make 目标时:
```bash
make build-deb VERSION="$RELEASE_VERSION"
```
如果构建目标会自动上传,而当前仅获构建授权,应改用其纯构建子目标。执行前检查
所需工具和环境,例如 Docker、`dpkg-deb`、编译器、SSH 访问或前端工具链。
不得擅自清理宽泛目录。若脚本包含 `rm -rf`,先解析并确认目标是明确、受限的构建目录。
### 4. 上传前检查
定位唯一目标产物;若匹配多个包,不凭文件时间猜测:
```bash
find <artifact-dir> -maxdepth 2 -type f -name '*.deb' -print
dpkg-deb --info <package.deb>
dpkg-deb --contents <package.deb>
```
至少验证:
- 文件存在、非空且 `dpkg-deb --info` 成功。
- `Package``Version``Architecture` 与本次发布一致。
- 包内容包含预期的主程序或关键文件。
- maintainer scripts 存在时权限正确,且没有明显的宿主机破坏性操作。
建议记录 SHA-256
```bash
sha256sum <package.deb>
```
### 5. 发布
优先调用项目已有上传目标或脚本,并通过环境变量注入凭据:
```bash
make upload-deb VERSION="$RELEASE_VERSION"
```
若项目上传脚本接受文件参数,传入刚刚校验过的确切路径。不要使用宽泛 glob。
若必须维护 Bash 上传脚本:
- 使用 `curl --fail-with-body` 或显式检查 HTTP 状态码。
- 只把 `200`/`201` 等服务端约定的状态判为成功。
- 用 trap 清理 `mktemp` 创建的响应文件。
- 在 `set -e` 下避免 `((count++))` 首次返回状态 1;使用
`count=$((count + 1))``((++count))`
- 多文件上传应汇总每个文件的结果,并在任一失败时返回非零。
### 6. 验证与汇报
发布成功不能只依据“curl 已执行”。综合检查:
- 上传命令退出码为零。
- HTTP 状态和响应体明确表示成功。
- 若仓库提供只读查询、索引或下载地址,再确认该包和版本已可见。
- 若索引更新是异步的,报告“上传已接受,索引尚待更新”,不要声称已完全可用。
最终回复给出:
- 包名、版本、架构。
- 产物路径和 SHA-256。
- 目标服务/仓库的非敏感标识。
- 构建、上传及仓库可见性各自的验证结果。
- 任何未完成项或回滚/覆盖风险。
## 修改已有发布逻辑时
- 保持项目现有变量名和调用入口,避免无关重构。
- 修复行为缺陷时增加最小静态检查或可离线运行的测试。
- 可用 `bash -n` 检查脚本语法;项目有 ShellCheck 时一并运行。
- 不通过真实生产上传来测试脚本,除非用户明确授权并给出测试版本或测试仓库。
## 完成标准
- 仅分析:入口、调用链、配置来源和风险已被准确说明。
- 仅构建:DEB 已生成,元数据、内容和校验和通过检查,未发生上传。
- 发布:构建检查通过,服务端接受上传,且仓库可见性已验证或被准确标记为待更新。
+67
View File
@@ -0,0 +1,67 @@
# skiff
`skiff` 用于创建、维护、安装和发布团队自研的 Agent Skill。Skill 的唯一来源位于
`~/.skills/skills/<name>/`,安装到各 Agent 时使用软链接。
## 什么时候使用
- 想把项目里的重复工作沉淀成一个 skill。
- 想完善、校验、转正或发布已有 skill。
- 想把自研 skill 安装到当前项目或全局 Agent。
- 想检查并修复 skill 软链接。
## 创建一个 skill
```bash
skiff create my-skill \
--idea "描述这个 skill 要解决的重复问题" \
--from-project .
```
命令会在 `~/.skills/.drafts/my-skill/` 创建:
- `SKILL.md`:给 Agent 阅读的工作流与约束。
- `README.md`:给人类阅读的用途、准备事项、示例和完成标准。
- `brief.yaml`:草稿来源信息,转正时自动移除。
完善 `SKILL.md``README.md` 后运行:
```bash
skiff check my-skill
skiff finalize my-skill
```
## 提交和发布
只提交:
```bash
skiff publish skills/my-skill -m "add my-skill"
```
提交并推送:
```bash
skiff publish skills/my-skill -m "add my-skill" --push
```
## 安装
安装到当前项目的 Codex
```bash
skiff add my-skill -a codex -y
```
安装到全局 Codex
```bash
skiff add my-skill -a codex -g -y
```
## 如何判断完成
- `skiff check <name>` 输出校验通过。
- 正式 skill 同时包含 `SKILL.md``README.md`
- `skiff status``skiff doctor` 显示目标软链接正常。
+5 -4
View File
@@ -38,15 +38,16 @@ skiff add <name> -a cursor -a claude -a codex -y
skiff create <name> --idea "<用户原始想法>" --from-project . skiff create <name> --idea "<用户原始想法>" --from-project .
``` ```
4. 编辑 `~/.skills/.drafts/<name>/SKILL.md`,完善触发条件、不适用场景、步骤、边界与验证方法。仅在确有必要时增加 `references/``scripts/``assets/` 4. 编辑 `~/.skills/.drafts/<name>/SKILL.md`,完善触发条件、不适用场景、步骤、边界与验证方法。
5. 不要把项目专属路径、私有业务规则、一次性命令或密钥复制到通用 skill 5. 编辑同目录的 `README.md`,用面向人类的语言说明用途、准备事项、可直接复制的请求示例、Agent 会做什么以及如何判断完成。README 不应复述 Agent 内部指令
6. 运行校验并修复所有问题: 6. 仅在确有必要时增加 `references/``scripts/``assets/`。不要把项目专属路径、私有业务规则、一次性命令或密钥复制到通用 skill。
7. 运行校验并修复所有问题:
```bash ```bash
skiff check <name> skiff check <name>
``` ```
7. 向用户展示名称、description、核心步骤和验证方式。获得确认后再转正: 8. 向用户展示名称、description、README 的人类使用方式、核心步骤和验证方式。获得确认后再转正:
```bash ```bash
skiff finalize <name> skiff finalize <name>
+41
View File
@@ -28,6 +28,13 @@ class CreateWorkflowTests(unittest.TestCase):
"---\n\n# Skill 名称\n\n## 步骤\n\n1. 第一步\n", "---\n\n# Skill 名称\n\n## 步骤\n\n1. 第一步\n",
encoding="utf-8", encoding="utf-8",
) )
template.joinpath("README.md").write_text(
"# skill-name\n\n"
"用一句话告诉使用者这个 skill 能解决什么问题。\n\n"
"给出用户可以直接说出的典型请求。\n\n"
"给出一条可以直接交给 Agent 的示例请求。\n",
encoding="utf-8",
)
project_skill = self.skills_home / "skills" / "skiff" project_skill = self.skills_home / "skills" / "skiff"
project_skill.mkdir() project_skill.mkdir()
project_skill.joinpath("SKILL.md").write_text( project_skill.joinpath("SKILL.md").write_text(
@@ -36,6 +43,10 @@ class CreateWorkflowTests(unittest.TestCase):
"---\n\n# skiff\n\n## 步骤\n\n1. 维护 skill。\n", "---\n\n# skiff\n\n## 步骤\n\n1. 维护 skill。\n",
encoding="utf-8", encoding="utf-8",
) )
project_skill.joinpath("README.md").write_text(
"# skiff\n\n供人类使用的 skiff 说明。\n",
encoding="utf-8",
)
def tearDown(self) -> None: def tearDown(self) -> None:
self.temp_dir.cleanup() self.temp_dir.cleanup()
@@ -109,6 +120,10 @@ class CreateWorkflowTests(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
draft = self.skills_home / ".drafts" / "migration-review" draft = self.skills_home / ".drafts" / "migration-review"
self.assertTrue((draft / "SKILL.md").is_file()) self.assertTrue((draft / "SKILL.md").is_file())
self.assertEqual(
(draft / "README.md").read_text(encoding="utf-8").splitlines()[0],
"# migration-review",
)
brief = (draft / "brief.yaml").read_text(encoding="utf-8") brief = (draft / "brief.yaml").read_text(encoding="utf-8")
self.assertIn("idea: 检查数据库迁移", brief) self.assertIn("idea: 检查数据库迁移", brief)
self.assertIn(f"source_project: {project.resolve()}", brief) self.assertIn(f"source_project: {project.resolve()}", brief)
@@ -134,12 +149,19 @@ class CreateWorkflowTests(unittest.TestCase):
"---\n\n# 数据库迁移检查\n\n## 步骤\n\n1. 检查迁移与回滚路径。\n", "---\n\n# 数据库迁移检查\n\n## 步骤\n\n1. 检查迁移与回滚路径。\n",
encoding="utf-8", encoding="utf-8",
) )
draft.joinpath("README.md").write_text(
"# migration-review\n\n"
"用于检查数据库迁移安全性。\n\n"
"## 使用示例\n\n请检查这次数据库迁移。\n",
encoding="utf-8",
)
result = self.run_skiff("finalize", "migration-review") result = self.run_skiff("finalize", "migration-review")
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
final = self.skills_home / "skills" / "migration-review" final = self.skills_home / "skills" / "migration-review"
self.assertTrue((final / "SKILL.md").is_file()) self.assertTrue((final / "SKILL.md").is_file())
self.assertTrue((final / "README.md").is_file())
self.assertFalse((final / "brief.yaml").exists()) self.assertFalse((final / "brief.yaml").exists())
self.assertFalse(draft.exists()) self.assertFalse(draft.exists())
@@ -152,12 +174,31 @@ class CreateWorkflowTests(unittest.TestCase):
"---\n\n# 参考检查\n\n读取 [规范](references/rules.md)。\n", "---\n\n# 参考检查\n\n读取 [规范](references/rules.md)。\n",
encoding="utf-8", encoding="utf-8",
) )
draft.joinpath("README.md").write_text(
"# reference-check\n\n用于检查本地参考资料。\n",
encoding="utf-8",
)
result = self.run_skiff("check", "reference-check") result = self.run_skiff("check", "reference-check")
self.assertNotEqual(result.returncode, 0) self.assertNotEqual(result.returncode, 0)
self.assertIn("引用文件不存在", result.stderr) self.assertIn("引用文件不存在", result.stderr)
def test_check_rejects_missing_readme(self) -> None:
draft = self.skills_home / ".drafts" / "missing-readme"
draft.mkdir(parents=True)
draft.joinpath("SKILL.md").write_text(
"---\nname: missing-readme\ndescription: >-\n"
" 检查 skill 是否包含人类使用说明。用于维护 skill 结构时。\n"
"---\n\n# README 检查\n\n1. 检查 README。\n",
encoding="utf-8",
)
result = self.run_skiff("check", "missing-readme")
self.assertNotEqual(result.returncode, 0)
self.assertIn("缺少 README.md", result.stderr)
def test_finalize_does_not_overwrite_existing_owned_skill(self) -> None: def test_finalize_does_not_overwrite_existing_owned_skill(self) -> None:
created = self.run_skiff("create", "collision-check", "--idea", "检查名称冲突") created = self.run_skiff("create", "collision-check", "--idea", "检查名称冲突")
self.assertEqual(created.returncode, 0, created.stderr) self.assertEqual(created.returncode, 0, created.stderr)