diff --git a/skiff/README.md b/skiff/README.md index 3c49665..5af0409 100644 --- a/skiff/README.md +++ b/skiff/README.md @@ -176,8 +176,8 @@ skiff 不保存 token。可以使用 `company/code-review`,也可以使用 | 命令 | 说明 | |------|------| -| `skiff create [--idea TEXT] [--from-project PATH]` | 从模板创建草稿到 `~/.skills/.drafts/` | -| `skiff check ` | 校验草稿或正式 skill | +| `skiff create [--idea TEXT] [--from-project PATH]` | 从模板创建含 `SKILL.md`、`README.md` 的草稿 | +| `skiff check ` | 校验草稿或正式 skill,包括人类使用说明 | | `skiff finalize ` | 校验草稿并移动到正式 `skills/` | | `skiff doctor [--target all] [--fix]` | 检查软链健康状态,`--fix` 自动修复 | @@ -187,7 +187,7 @@ skiff 不保存 token。可以使用 `company/code-review`,也可以使用 ```bash skiff create my-skill --idea "描述要解决的重复问题" --from-project . -# 由 Agent 完善 ~/.skills/.drafts/my-skill/SKILL.md +# 由 Agent 完善草稿中的 SKILL.md 和 README.md skiff check my-skill skiff finalize my-skill skiff publish skills/my-skill -m "add my-skill" --push diff --git a/skiff/cli.py b/skiff/cli.py index 23f217b..3fddf68 100644 --- a/skiff/cli.py +++ b/skiff/cli.py @@ -1001,6 +1001,16 @@ def cmd_create(args: argparse.Namespace) -> None: flags=re.MULTILINE, ) 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 = { "name": args.name, "idea": args.idea or "", diff --git a/skiff/skills.py b/skiff/skills.py index fad95e9..51424c4 100644 --- a/skiff/skills.py +++ b/skiff/skills.py @@ -196,8 +196,24 @@ def validate_skill_dir(skill_dir: Path, expected_name: str) -> list[str]: """返回 skill 目录中的校验问题;空列表表示通过。""" issues: list[str] = [] skill_md = skill_dir / "SKILL.md" + readme = skill_dir / "README.md" if not skill_md.is_file(): 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") if not text.strip(): diff --git a/skills/_template/README.md b/skills/_template/README.md new file mode 100644 index 0000000..92afb06 --- /dev/null +++ b/skills/_template/README.md @@ -0,0 +1,28 @@ +# skill-name + +用一句话告诉使用者这个 skill 能解决什么问题。 + +## 什么时候使用 + +- 给出用户可以直接说出的典型请求。 +- 说明适用范围。 + +## 使用前准备 + +- 列出需要用户提前提供或确认的信息、权限和依赖。 + +## 使用示例 + +```text +给出一条可以直接交给 Agent 的示例请求。 +``` + +## Agent 会做什么 + +1. 用面向使用者的语言概括主要步骤。 +2. 说明可能发生的外部写操作或重要边界。 + +## 如何判断完成 + +- 说明使用者最终会得到什么,以及如何确认结果正确。 + diff --git a/skills/deb-publisher/README.md b/skills/deb-publisher/README.md new file mode 100644 index 0000000..2209f77 --- /dev/null +++ b/skills/deb-publisher/README.md @@ -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 应明确说明“上传已接受,索引待更新”。 + diff --git a/skills/deb-publisher/SKILL.md b/skills/deb-publisher/SKILL.md new file mode 100644 index 0000000..c1dec3b --- /dev/null +++ b/skills/deb-publisher/SKILL.md @@ -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 -maxdepth 2 -type f -name '*.deb' -print +dpkg-deb --info +dpkg-deb --contents +``` + +至少验证: + +- 文件存在、非空且 `dpkg-deb --info` 成功。 +- `Package`、`Version`、`Architecture` 与本次发布一致。 +- 包内容包含预期的主程序或关键文件。 +- maintainer scripts 存在时权限正确,且没有明显的宿主机破坏性操作。 + +建议记录 SHA-256: + +```bash +sha256sum +``` + +### 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 已生成,元数据、内容和校验和通过检查,未发生上传。 +- 发布:构建检查通过,服务端接受上传,且仓库可见性已验证或被准确标记为待更新。 diff --git a/skills/skiff/README.md b/skills/skiff/README.md new file mode 100644 index 0000000..4230af0 --- /dev/null +++ b/skills/skiff/README.md @@ -0,0 +1,67 @@ +# skiff + +`skiff` 用于创建、维护、安装和发布团队自研的 Agent Skill。Skill 的唯一来源位于 +`~/.skills/skills//`,安装到各 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 ` 输出校验通过。 +- 正式 skill 同时包含 `SKILL.md` 和 `README.md`。 +- `skiff status` 或 `skiff doctor` 显示目标软链接正常。 + diff --git a/skills/skiff/SKILL.md b/skills/skiff/SKILL.md index c2e2b77..7d1a30a 100644 --- a/skills/skiff/SKILL.md +++ b/skills/skiff/SKILL.md @@ -38,15 +38,16 @@ skiff add -a cursor -a claude -a codex -y skiff create --idea "<用户原始想法>" --from-project . ``` -4. 编辑 `~/.skills/.drafts//SKILL.md`,完善触发条件、不适用场景、步骤、边界与验证方法。仅在确有必要时增加 `references/`、`scripts/` 或 `assets/`。 -5. 不要把项目专属路径、私有业务规则、一次性命令或密钥复制到通用 skill。 -6. 运行校验并修复所有问题: +4. 编辑 `~/.skills/.drafts//SKILL.md`,完善触发条件、不适用场景、步骤、边界与验证方法。 +5. 编辑同目录的 `README.md`,用面向人类的语言说明用途、准备事项、可直接复制的请求示例、Agent 会做什么以及如何判断完成。README 不应复述 Agent 内部指令。 +6. 仅在确有必要时增加 `references/`、`scripts/` 或 `assets/`。不要把项目专属路径、私有业务规则、一次性命令或密钥复制到通用 skill。 +7. 运行校验并修复所有问题: ```bash skiff check ``` -7. 向用户展示名称、description、核心步骤和验证方式。获得确认后再转正: +8. 向用户展示名称、description、README 的人类使用方式、核心步骤和验证方式。获得确认后再转正: ```bash skiff finalize diff --git a/tests/test_create_workflow.py b/tests/test_create_workflow.py index 90a5699..19e9166 100644 --- a/tests/test_create_workflow.py +++ b/tests/test_create_workflow.py @@ -28,6 +28,13 @@ class CreateWorkflowTests(unittest.TestCase): "---\n\n# Skill 名称\n\n## 步骤\n\n1. 第一步\n", 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.mkdir() project_skill.joinpath("SKILL.md").write_text( @@ -36,6 +43,10 @@ class CreateWorkflowTests(unittest.TestCase): "---\n\n# skiff\n\n## 步骤\n\n1. 维护 skill。\n", encoding="utf-8", ) + project_skill.joinpath("README.md").write_text( + "# skiff\n\n供人类使用的 skiff 说明。\n", + encoding="utf-8", + ) def tearDown(self) -> None: self.temp_dir.cleanup() @@ -109,6 +120,10 @@ class CreateWorkflowTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) draft = self.skills_home / ".drafts" / "migration-review" 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") self.assertIn("idea: 检查数据库迁移", brief) self.assertIn(f"source_project: {project.resolve()}", brief) @@ -134,12 +149,19 @@ class CreateWorkflowTests(unittest.TestCase): "---\n\n# 数据库迁移检查\n\n## 步骤\n\n1. 检查迁移与回滚路径。\n", 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") self.assertEqual(result.returncode, 0, result.stderr) final = self.skills_home / "skills" / "migration-review" self.assertTrue((final / "SKILL.md").is_file()) + self.assertTrue((final / "README.md").is_file()) self.assertFalse((final / "brief.yaml").exists()) self.assertFalse(draft.exists()) @@ -152,12 +174,31 @@ class CreateWorkflowTests(unittest.TestCase): "---\n\n# 参考检查\n\n读取 [规范](references/rules.md)。\n", encoding="utf-8", ) + draft.joinpath("README.md").write_text( + "# reference-check\n\n用于检查本地参考资料。\n", + encoding="utf-8", + ) result = self.run_skiff("check", "reference-check") self.assertNotEqual(result.returncode, 0) 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: created = self.run_skiff("create", "collision-check", "--idea", "检查名称冲突") self.assertEqual(created.returncode, 0, created.stderr)