feat: add skill draft workflow
This commit is contained in:
@@ -15,13 +15,12 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 克隆并关联
|
# 1. 克隆并关联
|
||||||
git clone https://git.yumee.top/laily/skills.git ~/code/gitea/skills
|
git clone https://git.yumee.top/laily/skills.git ~/.skills
|
||||||
git clone https://git.yumee.top/laily/skiff.git ~/code/gitea/skiff
|
cd ~/.skills && ./install.sh
|
||||||
cd ~/code/gitea/skiff && ./install.sh
|
|
||||||
skiff setup ~/code/gitea/skills
|
|
||||||
|
|
||||||
# 2. 全局安装 skill
|
# install.sh 会自动把 skiff 项目 skill 安装到所有 Agent
|
||||||
skiff install declarative-openspec-loop
|
# 2. 安装其他 skill
|
||||||
|
skiff add declarative-openspec-loop -g
|
||||||
|
|
||||||
# 3. 查看状态
|
# 3. 查看状态
|
||||||
skiff list
|
skiff list
|
||||||
@@ -61,6 +60,7 @@ AGENTS.md # 本文档
|
|||||||
|
|
||||||
| Skill | 说明 |
|
| Skill | 说明 |
|
||||||
| ---------------------------------------------------------------------- | ------------------------------------------------- |
|
| ---------------------------------------------------------------------- | ------------------------------------------------- |
|
||||||
|
| [skiff](skills/skiff/SKILL.md) | 本项目工作流:创建、使用、反馈与更新 owned skill |
|
||||||
| [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动 propose/apply/校验并迭代直到通过 |
|
| [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动 propose/apply/校验并迭代直到通过 |
|
||||||
| [discussion-notes](skills/discussion-notes/SKILL.md) | 讨论沉淀:边讨论边维护 Markdown 笔记,无 .raw.md |
|
| [discussion-notes](skills/discussion-notes/SKILL.md) | 讨论沉淀:边讨论边维护 Markdown 笔记,无 .raw.md |
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ targets: # 可选,默认 all
|
|||||||
| `skiff sync` | `npm ci` |
|
| `skiff sync` | `npm ci` |
|
||||||
|
|
||||||
|
|
||||||
项目级命令(skiff 待实现):
|
项目级命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/code/my-app
|
cd ~/code/my-app
|
||||||
@@ -212,7 +212,7 @@ skiff sync
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
| -------------------------------------- | ------------------- |
|
| -------------------------------------- | ------------------- |
|
||||||
| `skiff setup <path>` | 关联 `~/.skills` 到本仓库 |
|
| `skiff bootstrap` | 将 skiff 项目 skill 全局安装到所有 Agent |
|
||||||
| `skiff list` | 列出所有 skill |
|
| `skiff list` | 列出所有 skill |
|
||||||
| `skiff status` | 安装状态总览 |
|
| `skiff status` | 安装状态总览 |
|
||||||
| `skiff install <name>` | 全局安装(symlink) |
|
| `skiff install <name>` | 全局安装(symlink) |
|
||||||
@@ -220,15 +220,15 @@ skiff sync
|
|||||||
| `skiff add / fetch / install-external` | 外部 Git skill |
|
| `skiff add / fetch / install-external` | 外部 Git skill |
|
||||||
|
|
||||||
|
|
||||||
### 待实现
|
### 草稿与健康检查
|
||||||
|
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
| ------------------------ | --------------------------- |
|
| --- | --- |
|
||||||
| `skiff enable / disable` | 项目级启用/关闭 |
|
| `skiff create <name> --idea TEXT [--from-project PATH]` | 从 `_template/` 创建草稿 |
|
||||||
| `skiff sync` | 按 `.skills.yaml` 重建 symlink |
|
| `skiff check <name>` | 校验草稿或正式 skill |
|
||||||
| `skiff create` | 从 `_template/` 脚手架创建 skill |
|
| `skiff finalize <name>` | 校验并将草稿转为正式 skill |
|
||||||
| `skiff doctor` | symlink 健康检查 |
|
| `skiff doctor [--fix]` | symlink 健康检查与修复 |
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -263,11 +263,12 @@ description: >-
|
|||||||
|
|
||||||
### 新建流程
|
### 新建流程
|
||||||
|
|
||||||
1. `cp -r skills/_template skills/my-skill`
|
1. `skiff create my-skill --idea "..." --from-project .`
|
||||||
2. 编辑 `skills/my-skill/SKILL.md`
|
2. Agent 编辑 `~/.skills/.drafts/my-skill/SKILL.md`
|
||||||
3. `skiff install my-skill --target cursor` 验证
|
3. `skiff check my-skill`
|
||||||
4. 在本仓库 commit
|
4. 用户确认后执行 `skiff finalize my-skill`
|
||||||
5. 各项目 `skiff enable my-skill`(待实现)
|
5. `skiff add my-skill -a cursor -g -y` 验证
|
||||||
|
6. 在本仓库 commit;需要的项目再用 `skiff add my-skill` 启用
|
||||||
|
|
||||||
**禁止**在 `~/.cursor/skills/` 或项目 Agent 目录直接创建非 symlink 的 skill。
|
**禁止**在 `~/.cursor/skills/` 或项目 Agent 目录直接创建非 symlink 的 skill。
|
||||||
|
|
||||||
@@ -294,7 +295,7 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
|
|||||||
| -------------- | ----------------------------------------------------- |
|
| -------------- | ----------------------------------------------------- |
|
||||||
| Cursor / Codex | symlink,正常 |
|
| Cursor / Codex | symlink,正常 |
|
||||||
| Claude Code | symlink 单个 skill 目录,不要 symlink 整个 `~/.claude/skills/` |
|
| Claude Code | symlink 单个 skill 目录,不要 symlink 整个 `~/.claude/skills/` |
|
||||||
| symlink 被替换 | `skiff doctor`(待实现)→ 重建 symlink |
|
| symlink 被替换 | `skiff doctor --fix` → 重建 symlink |
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -317,8 +318,8 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
|
|||||||
|
|
||||||
| 我要… | 命令 | 在哪 |
|
| 我要… | 命令 | 在哪 |
|
||||||
| ---------- | --------------------------------- | ---- |
|
| ---------- | --------------------------------- | ---- |
|
||||||
| 首次 setup | `skiff setup ~/code/gitea/skills` | 任意 |
|
| 首次安装 | `git clone <repo> ~/.skills && ~/.skills/install.sh` | 任意 |
|
||||||
| 新建 skill | 复制 `_template/` → 编辑 → commit | 本仓库 |
|
| 新建 skill | `skiff create` → Agent 完善 → `check/finalize` | 任意项目 |
|
||||||
| 全局启用 | `skiff install <name>` | 任意 |
|
| 全局启用 | `skiff install <name>` | 任意 |
|
||||||
| 项目启用 | `skiff enable <name>` | 项目目录 |
|
| 项目启用 | `skiff enable <name>` | 项目目录 |
|
||||||
| 看状态 | `skiff status` | 任意 |
|
| 看状态 | `skiff status` | 任意 |
|
||||||
@@ -335,4 +336,3 @@ Claude Code 对 symlink 支持不稳定:可能无法发现 skill,或写入
|
|||||||
- [Vercel skills CLI](https://github.com/vercel-labs/skills)
|
- [Vercel skills CLI](https://github.com/vercel-labs/skills)
|
||||||
- [skills.sh](https://skills.sh)
|
- [skills.sh](https://skills.sh)
|
||||||
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
|
- [Cursor Skills 文档](https://cursor.com/docs/context/skills)
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,9 @@
|
|||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.yumee.top/laily/skills.git ~/code/gitea/skills
|
git clone https://git.yumee.top/laily/skills.git ~/.skills
|
||||||
cd ~/code/gitea/skills
|
cd ~/.skills
|
||||||
./install.sh
|
./install.sh # 安装 CLI,并将 skiff 项目 skill 安装到所有 Agent
|
||||||
skiff setup ~/code/gitea/skills
|
|
||||||
|
|
||||||
skiff install declarative-openspec-loop
|
skiff install declarative-openspec-loop
|
||||||
skiff list
|
skiff list
|
||||||
@@ -38,6 +37,7 @@ AGENTS.md # 详细规范与架构说明
|
|||||||
|
|
||||||
| Skill | 说明 |
|
| Skill | 说明 |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
|
| [skiff](skills/skiff/SKILL.md) | 在项目中创建、安装、反馈和维护 owned skill |
|
||||||
| [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动迭代直到通过 |
|
| [declarative-openspec-loop](skills/declarative-openspec-loop/SKILL.md) | 声明式编程循环:用户提供校验方式,Agent 自动迭代直到通过 |
|
||||||
| [discussion-notes](skills/discussion-notes/SKILL.md) | 讨论沉淀:边讨论边维护 Markdown 笔记 |
|
| [discussion-notes](skills/discussion-notes/SKILL.md) | 讨论沉淀:边讨论边维护 Markdown 笔记 |
|
||||||
|
|
||||||
@@ -52,11 +52,15 @@ AGENTS.md # 详细规范与架构说明
|
|||||||
新建 skill:
|
新建 skill:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff create my-skill # 从 _template/ 脚手架创建
|
skiff create my-skill --idea "描述要解决的重复问题" --from-project .
|
||||||
# 编辑 skills/my-skill/SKILL.md
|
# 由 Agent 完善 ~/.skills/.drafts/my-skill/SKILL.md
|
||||||
skiff install my-skill # 全局安装验证
|
skiff check my-skill
|
||||||
|
skiff finalize my-skill
|
||||||
|
skiff add my-skill -g # 全局安装验证
|
||||||
```
|
```
|
||||||
|
|
||||||
|
项目里使用 skill 发现通用问题或优化时,让 Agent 按 `skiff` skill 收集实际结果与期望结果,修改 `~/.skills/skills/<name>/` 的 SSOT,并执行 `skiff check <name>`。项目专属规则保留在项目内,不回流到通用 skill。
|
||||||
|
|
||||||
## 安装方式
|
## 安装方式
|
||||||
|
|
||||||
### 全局(用户级)
|
### 全局(用户级)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ mkdir -p "$BIN_DIR"
|
|||||||
ln -sf "$REPO/bin/skiff" "$BIN_DIR/skiff"
|
ln -sf "$REPO/bin/skiff" "$BIN_DIR/skiff"
|
||||||
echo "已安装 skiff -> $BIN_DIR/skiff"
|
echo "已安装 skiff -> $BIN_DIR/skiff"
|
||||||
|
|
||||||
|
PYTHONPATH="$REPO${PYTHONPATH:+:$PYTHONPATH}" python3 -m skiff bootstrap
|
||||||
|
|
||||||
path_already_configured() {
|
path_already_configured() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
[[ -f "$file" ]] && grep -qF "$PATH_MARKER" "$file"
|
[[ -f "$file" ]] && grep -qF "$PATH_MARKER" "$file"
|
||||||
|
|||||||
+2
-7
@@ -6,12 +6,7 @@
|
|||||||
# path: <subpath> (default: .)
|
# path: <subpath> (default: .)
|
||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
# superpowers:
|
# example-skills:
|
||||||
# repo: https://github.com/obra/superpowers
|
# repo: https://github.com/example/skills
|
||||||
# ref: main
|
# ref: main
|
||||||
# path: .
|
# path: .
|
||||||
|
|
||||||
superpowers:
|
|
||||||
repo: https://github.com/obra/superpowers
|
|
||||||
ref: main
|
|
||||||
path: .
|
|
||||||
|
|||||||
+21
-12
@@ -37,22 +37,21 @@ skiff publish skills/discussion-notes -m "update discussion-notes" --push
|
|||||||
PYTHONPATH=/path/to/skills python3 -m skiff <command>
|
PYTHONPATH=/path/to/skills python3 -m skiff <command>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 首次配置
|
## 首次安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff setup ~/code/gitea/skills # 将 ~/.skills 软链到仓库
|
git clone https://git.yumee.top/laily/skills.git ~/.skills
|
||||||
|
~/.skills/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
若仓库本身就在 `~/.skills`(开发场景),`setup` 会自动识别,无需额外操作。
|
`install.sh` 会安装 CLI,并自动执行 `skiff bootstrap`,将本仓库的 `skiff` skill 全局软链到 Cursor、Claude Code 和 Codex。也可以随时手动重跑:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
## 命令参考
|
## 命令参考
|
||||||
|
|
||||||
### 仓库关联
|
|
||||||
|
|
||||||
| 命令 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `skiff setup <path>` | 关联 `~/.skills` 到 skills 仓库 |
|
|
||||||
|
|
||||||
### 查看
|
### 查看
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
@@ -60,6 +59,12 @@ skiff setup ~/code/gitea/skills # 将 ~/.skills 软链到仓库
|
|||||||
| `skiff list` | 列出自研 skill 与 registry 中的外部 skill |
|
| `skiff list` | 列出自研 skill 与 registry 中的外部 skill |
|
||||||
| `skiff status [--target all\|cursor\|claude\|codex]` | 安装状态总览 |
|
| `skiff status [--target all\|cursor\|claude\|codex]` | 安装状态总览 |
|
||||||
|
|
||||||
|
### 项目初始化
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `skiff bootstrap` | 将本项目的 `skiff` skill 全局安装到所有 Agent |
|
||||||
|
|
||||||
### 全局安装(自研 skill)
|
### 全局安装(自研 skill)
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
@@ -107,7 +112,9 @@ skiff setup ~/code/gitea/skills # 将 ~/.skills 软链到仓库
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `skiff create <name>` | 从 `skills/_template/` 创建自研 skill |
|
| `skiff create <name> [--idea TEXT] [--from-project PATH]` | 从模板创建草稿到 `~/.skills/.drafts/` |
|
||||||
|
| `skiff check <name>` | 校验草稿或正式 skill |
|
||||||
|
| `skiff finalize <name>` | 校验草稿并移动到正式 `skills/` |
|
||||||
| `skiff doctor [--target all] [--fix]` | 检查软链健康状态,`--fix` 自动修复 |
|
| `skiff doctor [--target all] [--fix]` | 检查软链健康状态,`--fix` 自动修复 |
|
||||||
|
|
||||||
## 常用工作流
|
## 常用工作流
|
||||||
@@ -115,8 +122,10 @@ skiff setup ~/code/gitea/skills # 将 ~/.skills 软链到仓库
|
|||||||
### 新建并全局启用自研 skill
|
### 新建并全局启用自研 skill
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff create my-skill
|
skiff create my-skill --idea "描述要解决的重复问题" --from-project .
|
||||||
# 编辑 skills/my-skill/SKILL.md
|
# 由 Agent 完善 ~/.skills/.drafts/my-skill/SKILL.md
|
||||||
|
skiff check 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
|
||||||
skiff add my-skill -a cursor -g -y
|
skiff add my-skill -a cursor -g -y
|
||||||
skiff doctor -a cursor
|
skiff doctor -a cursor
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
"""skiff — Agent Skills 安装与管理 CLI。"""
|
"""skiff — Agent Skills 安装与管理 CLI。"""
|
||||||
|
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.4.0"
|
||||||
|
|||||||
+91
-32
@@ -6,6 +6,7 @@ import argparse
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from skiff import __version__
|
from skiff import __version__
|
||||||
@@ -13,6 +14,7 @@ from skiff.agents import flatten_agent_args, resolve_agent_args
|
|||||||
from skiff.gitops import publish as git_publish
|
from skiff.gitops import publish as git_publish
|
||||||
from skiff.paths import (
|
from skiff.paths import (
|
||||||
ALL_TARGETS,
|
ALL_TARGETS,
|
||||||
|
DRAFTS_DIR,
|
||||||
EXTERNALS_DIR,
|
EXTERNALS_DIR,
|
||||||
SKILLS_DIR,
|
SKILLS_DIR,
|
||||||
SKILLS_HOME,
|
SKILLS_HOME,
|
||||||
@@ -34,8 +36,10 @@ from skiff.skills import (
|
|||||||
owned_skill_path,
|
owned_skill_path,
|
||||||
resolve_skill_source,
|
resolve_skill_source,
|
||||||
skill_description,
|
skill_description,
|
||||||
|
validate_skill_dir,
|
||||||
validate_skill_name,
|
validate_skill_name,
|
||||||
)
|
)
|
||||||
|
from skiff.yaml_io import safe_dump
|
||||||
from skiff.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
|
from skiff.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
|
||||||
|
|
||||||
|
|
||||||
@@ -130,29 +134,6 @@ def _remove_skill(
|
|||||||
return removed
|
return removed
|
||||||
|
|
||||||
|
|
||||||
def cmd_setup(args: argparse.Namespace) -> None:
|
|
||||||
repo = Path(args.path).resolve()
|
|
||||||
if not (repo / "skills").is_dir():
|
|
||||||
raise SystemExit(f"不是有效的 skills 仓库(缺少 skills/): {repo}")
|
|
||||||
|
|
||||||
if repo == SKILLS_HOME.resolve() and SKILLS_HOME.is_dir() and not SKILLS_HOME.is_symlink():
|
|
||||||
_print(f"skills 仓库已在 ~/.skills: {repo}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if SKILLS_HOME.is_symlink():
|
|
||||||
current = SKILLS_HOME.resolve()
|
|
||||||
if current == repo:
|
|
||||||
_print(f"已关联: ~/.skills -> {repo}")
|
|
||||||
return
|
|
||||||
SKILLS_HOME.unlink()
|
|
||||||
elif SKILLS_HOME.exists():
|
|
||||||
raise SystemExit(f"~/.skills 已存在且不是软链: {SKILLS_HOME}")
|
|
||||||
|
|
||||||
SKILLS_HOME.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
SKILLS_HOME.symlink_to(repo)
|
|
||||||
_print(f"已关联: ~/.skills -> {repo}")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_list(args: argparse.Namespace) -> None:
|
def cmd_list(args: argparse.Namespace) -> None:
|
||||||
ensure_skills_home()
|
ensure_skills_home()
|
||||||
owned = list_owned_skills()
|
owned = list_owned_skills()
|
||||||
@@ -171,6 +152,15 @@ def cmd_list(args: argparse.Namespace) -> None:
|
|||||||
_print(f" {name} ({repo})")
|
_print(f" {name} ({repo})")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_bootstrap(args: argparse.Namespace) -> None:
|
||||||
|
del args
|
||||||
|
ensure_skills_home()
|
||||||
|
project_skill = "skiff"
|
||||||
|
owned_skill_path(project_skill)
|
||||||
|
_install_skill(project_skill, list(ALL_TARGETS), project_root=None)
|
||||||
|
_print("已安装项目 skill 到所有 agent")
|
||||||
|
|
||||||
|
|
||||||
def _installed_links(name: str, targets: list[str], project_root: Path | None = None) -> list[tuple[str, Path, Path]]:
|
def _installed_links(name: str, targets: list[str], project_root: Path | None = None) -> list[tuple[str, Path, Path]]:
|
||||||
skill_path, _ = resolve_skill_source(name)
|
skill_path, _ = resolve_skill_source(name)
|
||||||
rows: list[tuple[str, Path, Path]] = []
|
rows: list[tuple[str, Path, Path]] = []
|
||||||
@@ -386,7 +376,7 @@ def cmd_create(args: argparse.Namespace) -> None:
|
|||||||
if not TEMPLATE_DIR.is_dir():
|
if not TEMPLATE_DIR.is_dir():
|
||||||
raise SystemExit(f"模板目录不存在: {TEMPLATE_DIR}")
|
raise SystemExit(f"模板目录不存在: {TEMPLATE_DIR}")
|
||||||
|
|
||||||
dst = SKILLS_DIR / args.name
|
dst = DRAFTS_DIR / args.name
|
||||||
copy_template(TEMPLATE_DIR, dst)
|
copy_template(TEMPLATE_DIR, dst)
|
||||||
|
|
||||||
skill_md = dst / "SKILL.md"
|
skill_md = dst / "SKILL.md"
|
||||||
@@ -399,10 +389,67 @@ 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")
|
||||||
_print(f"已创建 skill: {dst}")
|
brief = {
|
||||||
_print(f"下一步: 编辑 {skill_md}")
|
"name": args.name,
|
||||||
_print(f" skiff publish skills/{args.name} -m \"add {args.name}\" --push")
|
"idea": args.idea or "",
|
||||||
_print(f" skiff add {args.name} -a cursor -g -y")
|
"source_project": str(Path(args.from_project).resolve()) if args.from_project else "",
|
||||||
|
"status": "draft",
|
||||||
|
"created_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||||
|
}
|
||||||
|
(dst / "brief.yaml").write_text(safe_dump(brief), encoding="utf-8")
|
||||||
|
_print(f"草稿已创建: {dst}")
|
||||||
|
_print(f"下一步: 请完善 skiff 草稿 {args.name}")
|
||||||
|
_print(f"完成后运行: skiff check {args.name} && skiff finalize {args.name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_or_owned_path(name: str) -> tuple[Path, str]:
|
||||||
|
draft = DRAFTS_DIR / name
|
||||||
|
if draft.is_dir():
|
||||||
|
return draft, "草稿"
|
||||||
|
owned = SKILLS_DIR / name
|
||||||
|
if owned.is_dir():
|
||||||
|
return owned, "正式 skill"
|
||||||
|
raise SystemExit(f"找不到草稿或正式 skill: {name}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_check(args: argparse.Namespace) -> None:
|
||||||
|
ensure_skills_home()
|
||||||
|
validate_skill_name(args.name)
|
||||||
|
path, kind = _draft_or_owned_path(args.name)
|
||||||
|
issues = validate_skill_dir(path, args.name)
|
||||||
|
if kind == "草稿":
|
||||||
|
issues = [issue for issue in issues if "草稿文件: brief.yaml" not in issue]
|
||||||
|
if issues:
|
||||||
|
for issue in issues:
|
||||||
|
_err(f"✗ {issue}")
|
||||||
|
raise SystemExit(1)
|
||||||
|
_print(f"✓ 校验通过 ({kind}): {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_finalize(args: argparse.Namespace) -> None:
|
||||||
|
ensure_skills_home()
|
||||||
|
validate_skill_name(args.name)
|
||||||
|
draft = DRAFTS_DIR / args.name
|
||||||
|
if not draft.is_dir():
|
||||||
|
raise SystemExit(f"草稿不存在: {args.name}")
|
||||||
|
final = SKILLS_DIR / args.name
|
||||||
|
if final.exists():
|
||||||
|
raise SystemExit(f"正式 skill 已存在: {final}")
|
||||||
|
|
||||||
|
issues = validate_skill_dir(draft, args.name)
|
||||||
|
issues = [issue for issue in issues if "草稿文件: brief.yaml" not in issue]
|
||||||
|
if issues:
|
||||||
|
for issue in issues:
|
||||||
|
_err(f"✗ {issue}")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
final.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
draft.replace(final)
|
||||||
|
brief = final / "brief.yaml"
|
||||||
|
if brief.exists():
|
||||||
|
brief.unlink()
|
||||||
|
_print(f"已完成 skill: {final}")
|
||||||
|
_print(f"下一步: skiff publish skills/{args.name} -m \"add {args.name}\" --push")
|
||||||
|
|
||||||
|
|
||||||
def cmd_doctor(args: argparse.Namespace) -> None:
|
def cmd_doctor(args: argparse.Namespace) -> None:
|
||||||
@@ -483,9 +530,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
p_setup = sub.add_parser("setup", help="关联 ~/.skills 到 skills 仓库")
|
p_bootstrap = sub.add_parser(
|
||||||
p_setup.add_argument("path", nargs="?", default=str(SKILLS_HOME), help="仓库路径(默认 ~/.skills)")
|
"bootstrap",
|
||||||
p_setup.set_defaults(func=cmd_setup)
|
help="将本项目 skiff skill 全局安装到所有 agent",
|
||||||
|
)
|
||||||
|
p_bootstrap.set_defaults(func=cmd_bootstrap)
|
||||||
|
|
||||||
p_list = sub.add_parser("list", help="列出 ~/.skills 中的 skill 目录")
|
p_list = sub.add_parser("list", help="列出 ~/.skills 中的 skill 目录")
|
||||||
p_list.set_defaults(func=cmd_list)
|
p_list.set_defaults(func=cmd_list)
|
||||||
@@ -557,10 +606,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
p_sync.add_argument("--project")
|
p_sync.add_argument("--project")
|
||||||
p_sync.set_defaults(func=cmd_sync)
|
p_sync.set_defaults(func=cmd_sync)
|
||||||
|
|
||||||
p_create = sub.add_parser("create", help="从 _template 创建自研 skill")
|
p_create = sub.add_parser("create", help="从 _template 创建自研 skill 草稿")
|
||||||
p_create.add_argument("name", help="skill 名称")
|
p_create.add_argument("name", help="skill 名称")
|
||||||
|
p_create.add_argument("--idea", help="创建 skill 的原始想法")
|
||||||
|
p_create.add_argument("--from-project", help="想法来源项目(仅记录上下文)")
|
||||||
p_create.set_defaults(func=cmd_create)
|
p_create.set_defaults(func=cmd_create)
|
||||||
|
|
||||||
|
p_check = sub.add_parser("check", help="校验草稿或正式 skill")
|
||||||
|
p_check.add_argument("name", help="skill 名称")
|
||||||
|
p_check.set_defaults(func=cmd_check)
|
||||||
|
|
||||||
|
p_finalize = sub.add_parser("finalize", help="校验草稿并转为正式 skill")
|
||||||
|
p_finalize.add_argument("name", help="skill 名称")
|
||||||
|
p_finalize.set_defaults(func=cmd_finalize)
|
||||||
|
|
||||||
p_doctor = sub.add_parser("doctor", help="软链健康检查")
|
p_doctor = sub.add_parser("doctor", help="软链健康检查")
|
||||||
p_doctor.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
p_doctor.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||||
p_doctor.add_argument("--fix", action="store_true", help="自动修复可修复的软链")
|
p_doctor.add_argument("--fix", action="store_true", help="自动修复可修复的软链")
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@ HOME = Path.home()
|
|||||||
SKILLS_HOME = HOME / ".skills"
|
SKILLS_HOME = HOME / ".skills"
|
||||||
SKILLS_DIR = SKILLS_HOME / "skills"
|
SKILLS_DIR = SKILLS_HOME / "skills"
|
||||||
TEMPLATE_DIR = SKILLS_DIR / "_template"
|
TEMPLATE_DIR = SKILLS_DIR / "_template"
|
||||||
|
DRAFTS_DIR = SKILLS_HOME / ".drafts"
|
||||||
REGISTRY_FILE = SKILLS_HOME / "registry.yaml"
|
REGISTRY_FILE = SKILLS_HOME / "registry.yaml"
|
||||||
EXTERNALS_DIR = HOME / ".local" / "share" / "skills" / "externals"
|
EXTERNALS_DIR = HOME / ".local" / "share" / "skills" / "externals"
|
||||||
PROJECT_MANIFEST = ".skills.yaml"
|
PROJECT_MANIFEST = ".skills.yaml"
|
||||||
@@ -44,5 +45,5 @@ def agent_skill_dir(target: str, *, project_root: Path | None = None) -> Path:
|
|||||||
def ensure_skills_home() -> None:
|
def ensure_skills_home() -> None:
|
||||||
if not SKILLS_HOME.is_dir():
|
if not SKILLS_HOME.is_dir():
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"~/.skills 未配置。请先运行: skiff setup <skills-repo-path>"
|
"~/.skills 不存在。请将 skills 仓库克隆到 ~/.skills"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -102,3 +102,68 @@ def validate_skill_name(name: str) -> None:
|
|||||||
)
|
)
|
||||||
if name == "_template":
|
if name == "_template":
|
||||||
raise SystemExit("不能使用保留名 _template")
|
raise SystemExit("不能使用保留名 _template")
|
||||||
|
|
||||||
|
|
||||||
|
_PLACEHOLDER_PATTERNS = (
|
||||||
|
(r"^name:\s*skill-name\s*$", "skill-name"),
|
||||||
|
(r"^#\s+Skill 名称\s*$", "Skill 名称"),
|
||||||
|
(r"简要描述 skill 做什么、何时触发", "简要描述 skill 做什么"),
|
||||||
|
(r"^-\s*触发场景 1\s*$", "触发场景 1"),
|
||||||
|
(r"^\d+\.\s*第一步\s*$", "第一步"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_skill_dir(skill_dir: Path, expected_name: str) -> list[str]:
|
||||||
|
"""返回 skill 目录中的校验问题;空列表表示通过。"""
|
||||||
|
issues: list[str] = []
|
||||||
|
skill_md = skill_dir / "SKILL.md"
|
||||||
|
if not skill_md.is_file():
|
||||||
|
return ["缺少 SKILL.md"]
|
||||||
|
|
||||||
|
text = skill_md.read_text(encoding="utf-8")
|
||||||
|
if not text.strip():
|
||||||
|
return ["SKILL.md 为空"]
|
||||||
|
if not text.startswith("---\n"):
|
||||||
|
return ["SKILL.md 缺少 YAML frontmatter"]
|
||||||
|
|
||||||
|
end = text.find("\n---", 4)
|
||||||
|
if end == -1:
|
||||||
|
return ["SKILL.md frontmatter 未闭合"]
|
||||||
|
frontmatter = text[4:end]
|
||||||
|
keys = re.findall(r"^([A-Za-z0-9_-]+):", frontmatter, re.MULTILINE)
|
||||||
|
unexpected = sorted(set(keys) - {"name", "description"})
|
||||||
|
missing = sorted({"name", "description"} - set(keys))
|
||||||
|
if missing:
|
||||||
|
issues.append(f"frontmatter 缺少字段: {', '.join(missing)}")
|
||||||
|
if unexpected:
|
||||||
|
issues.append(f"frontmatter 只允许 name、description,发现: {', '.join(unexpected)}")
|
||||||
|
|
||||||
|
meta = read_skill_meta(skill_dir)
|
||||||
|
if meta.get("name") != expected_name:
|
||||||
|
issues.append(
|
||||||
|
f"目录名与 frontmatter name 不一致: {expected_name} != {meta.get('name', '(缺失)')}"
|
||||||
|
)
|
||||||
|
description = meta.get("description", "").strip()
|
||||||
|
if not description:
|
||||||
|
issues.append("description 不能为空")
|
||||||
|
|
||||||
|
found_placeholders = [
|
||||||
|
label
|
||||||
|
for pattern, label in _PLACEHOLDER_PATTERNS
|
||||||
|
if re.search(pattern, text, re.MULTILINE | re.IGNORECASE)
|
||||||
|
]
|
||||||
|
if found_placeholders:
|
||||||
|
issues.append(f"存在模板占位内容: {', '.join(found_placeholders)}")
|
||||||
|
|
||||||
|
link_pattern = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
|
||||||
|
for target in link_pattern.findall(text):
|
||||||
|
target = target.strip().split("#", 1)[0]
|
||||||
|
if not target or "://" in target or target.startswith(("mailto:", "/")):
|
||||||
|
continue
|
||||||
|
if not (skill_dir / target).resolve().is_file():
|
||||||
|
issues.append(f"引用文件不存在: {target}")
|
||||||
|
|
||||||
|
for draft_file in ("brief.yaml",):
|
||||||
|
if (skill_dir / draft_file).exists():
|
||||||
|
issues.append(f"正式 skill 不应包含草稿文件: {draft_file}")
|
||||||
|
return issues
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: skill-name
|
name: skill-name
|
||||||
description: >-
|
description: >-
|
||||||
简要描述 skill 做什么、何时触发。Include trigger keywords so agents can match automatically.
|
简要描述 skill 做什么、何时触发。写入用户可能使用的触发表达,让 Agent 能自动匹配。
|
||||||
---
|
---
|
||||||
|
|
||||||
# Skill 名称
|
# Skill 名称
|
||||||
@@ -15,6 +15,10 @@ description: >-
|
|||||||
- 触发场景 1
|
- 触发场景 1
|
||||||
- 触发场景 2
|
- 触发场景 2
|
||||||
|
|
||||||
|
## 不适用
|
||||||
|
|
||||||
|
- 不应触发的相近场景
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 步骤
|
## 步骤
|
||||||
@@ -28,3 +32,7 @@ description: >-
|
|||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 约束或边界条件
|
- 约束或边界条件
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 说明如何确认 skill 的执行结果正确
|
||||||
|
|||||||
+78
-15
@@ -1,8 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: skiff
|
name: skiff
|
||||||
description: >-
|
description: >-
|
||||||
维护 ~/.skills 自研 skill:编辑后用 skiff publish 提交推送,用 skiff add/remove 在各项目或全局挂卸 skill。
|
创建和维护 ~/.skills 自研 skill:把项目开发中产生的想法提炼为草稿,完善并校验后发布,
|
||||||
触发词:skiff、自研 skill、publish skill、安装自研 skill、更新 skill 到项目。
|
或用 skiff add/remove 在项目及全局挂卸 skill。触发词:skiff、自研 skill、创建 skill、
|
||||||
|
想做一个 skill、publish skill、安装自研 skill、更新 skill 到项目。
|
||||||
---
|
---
|
||||||
|
|
||||||
# skiff 自研 Skill 工作流
|
# skiff 自研 Skill 工作流
|
||||||
@@ -11,29 +12,87 @@ SSOT 固定在 `~/.skills/skills/<name>/`。内容通过 **symlink** 分发到
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 日常流程
|
## 在项目中使用 skill
|
||||||
|
|
||||||
### 1. 改 skill
|
先浏览可用的 owned skill,再安装到当前项目:
|
||||||
|
|
||||||
直接编辑:
|
```bash
|
||||||
|
skiff add --list
|
||||||
```
|
skiff add <name> -a cursor -a claude -a codex -y
|
||||||
~/.skills/skills/<name>/SKILL.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 提交并推送(不用离开当前目录)
|
`skiff add <name>` 默认安装到当前项目;只有用户明确需要所有项目使用时才加 `-g`。安装结果是指向 `~/.skills/skills/<name>/` 的软链,不要在 Agent 目录创建副本。
|
||||||
|
|
||||||
|
## 创建新的 skill
|
||||||
|
|
||||||
|
### 从项目想法创建 skill
|
||||||
|
|
||||||
|
当用户在项目开发中提出“想创建一个 skill”时:
|
||||||
|
|
||||||
|
1. 用一句话确认它要解决的重复问题,并建议符合小写连字符规范的名称;信息足够时不要为了形式追问。
|
||||||
|
2. 读取当前项目中与想法直接相关的代码和规范,区分可复用工作流与项目私有事实。
|
||||||
|
3. 创建草稿:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff create <name> --idea "<用户原始想法>" --from-project .
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 编辑 `~/.skills/.drafts/<name>/SKILL.md`,完善触发条件、不适用场景、步骤、边界与验证方法。仅在确有必要时增加 `references/`、`scripts/` 或 `assets/`。
|
||||||
|
5. 不要把项目专属路径、私有业务规则、一次性命令或密钥复制到通用 skill。
|
||||||
|
6. 运行校验并修复所有问题:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff check <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 向用户展示名称、description、核心步骤和验证方式。获得确认后再转正:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff finalize <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
转正不会自动 commit、push 或安装。用户明确要求后再执行 `skiff publish` 或 `skiff add`。
|
||||||
|
|
||||||
|
## 问题或优化回流
|
||||||
|
|
||||||
|
在项目里使用 skills 遇到问题,或者发现可复用的优化时:
|
||||||
|
|
||||||
|
1. 先记录最小证据:触发用户表达、使用的 skill 名称、实际结果、期望结果,以及能复现问题的必要项目上下文。
|
||||||
|
2. 判断归属:
|
||||||
|
- 通用工作流、触发条件或验证缺陷:回流 owned skill。
|
||||||
|
- 仅当前项目成立的命令、路径、业务规则:留在项目文档或项目配置,不写回通用 skill。
|
||||||
|
- CLI 安装、软链或校验行为异常:修改 `~/.skills/skiff/` 中的 CLI 和测试。
|
||||||
|
- 第三方 skill:不要复制成 owned skill 或直接改安装目录;整理证据反馈上游,除非用户明确决定维护 fork。
|
||||||
|
3. 确认真实来源。Agent 目录通常是软链,owned skill 的 SSOT 固定为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.skills/skills/<name>/
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 修改 SSOT。行为修复应先补能复现问题的测试或示例,再改 `SKILL.md`、引用文件或脚本。
|
||||||
|
5. 校验并在原项目重跑最初失败的场景:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skiff check <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
6. 汇报修改内容、验证结果和影响范围。只有用户明确要求提交或推送时才运行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff publish skills/<name> -m "update <name>" --push
|
skiff publish skills/<name> -m "update <name>" --push
|
||||||
```
|
```
|
||||||
|
|
||||||
只暂存不提交:
|
软链正确时无需重新安装;SSOT 保存后项目立即读取新内容。
|
||||||
|
|
||||||
|
## 安装与维护
|
||||||
|
|
||||||
|
安装本项目的 `skiff` skill 到所有 Agent:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff publish skills/<name> --no-commit
|
skiff bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 装到项目或全局
|
安装其他 skill:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 当前项目
|
# 当前项目
|
||||||
@@ -47,7 +106,7 @@ skiff add discussion-notes -a cursor -g -y
|
|||||||
skiff add discussion-notes -a cursor -a codex -g -y
|
skiff add discussion-notes -a cursor -a codex -g -y
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 卸载
|
卸载:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff remove discussion-notes -a cursor -y # 当前项目
|
skiff remove discussion-notes -a cursor -y # 当前项目
|
||||||
@@ -55,7 +114,7 @@ skiff remove discussion-notes -g -a cursor -y # 全局
|
|||||||
skiff rm discussion-notes -g -y # rm 别名
|
skiff rm discussion-notes -g -y # rm 别名
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 浏览可用自研 skill
|
浏览可用自研 skill:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
skiff add --list
|
skiff add --list
|
||||||
@@ -76,18 +135,22 @@ skiff add --list
|
|||||||
|
|
||||||
| skiff | 说明 |
|
| skiff | 说明 |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
|
| `bootstrap` | 将本项目的 `skiff` skill 全局安装到所有 Agent |
|
||||||
| `add <name> [-g] [-a AGENT...] [-y]` | 安装 |
|
| `add <name> [-g] [-a AGENT...] [-y]` | 安装 |
|
||||||
| `remove <name> [-g] [-a AGENT...] [-y]` | 卸载(`rm` / `r` 别名) |
|
| `remove <name> [-g] [-a AGENT...] [-y]` | 卸载(`rm` / `r` 别名) |
|
||||||
| `add --list` | 列出可用自研 skill |
|
| `add --list` | 列出可用自研 skill |
|
||||||
| `publish [paths] -m MSG [--push]` | git add / commit / push |
|
| `publish [paths] -m MSG [--push]` | git add / commit / push |
|
||||||
| `list` | 列出 ~/.skills 目录结构 |
|
| `list` | 列出 ~/.skills 目录结构 |
|
||||||
| `status` | 查看软链安装状态 |
|
| `status` | 查看软链安装状态 |
|
||||||
| `create <name>` | 新建自研 skill |
|
| `create <name> --idea TEXT [--from-project PATH]` | 创建自研 skill 草稿 |
|
||||||
|
| `check <name>` | 校验草稿或正式 skill |
|
||||||
|
| `finalize <name>` | 校验草稿并转为正式 skill |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
|
|
||||||
- 不要在 `project/.agents/skills/` 里直接改文件;应改 `~/.skills/skills/` 再 `publish`
|
- 不要在 `project/.agents/skills/` 里直接改文件;应改 `~/.skills/skills/` 再 `publish`
|
||||||
|
- 未完成的内容保留在 `~/.skills/.drafts/`,不要直接放进正式 `skills/`
|
||||||
- symlink 正确时,**不需要 reinstall**;保存 SSOT 后各项目自动读到新内容
|
- symlink 正确时,**不需要 reinstall**;保存 SSOT 后各项目自动读到新内容
|
||||||
- 社区 skill 用 `npx skills add`,不要用 skiff `registry add` 除非团队要 pin 版本
|
- 社区 skill 用 `npx skills add`,不要用 skiff `registry add` 除非团队要 pin 版本
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateWorkflowTests(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.temp_dir = tempfile.TemporaryDirectory()
|
||||||
|
self.home = Path(self.temp_dir.name)
|
||||||
|
self.skills_home = self.home / ".skills"
|
||||||
|
template = self.skills_home / "skills" / "_template"
|
||||||
|
template.mkdir(parents=True)
|
||||||
|
template.joinpath("SKILL.md").write_text(
|
||||||
|
"---\nname: skill-name\ndescription: >-\n"
|
||||||
|
" 简要描述 skill 做什么、何时触发。Include trigger keywords so agents can match automatically.\n"
|
||||||
|
"---\n\n# Skill 名称\n\n## 步骤\n\n1. 第一步\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
project_skill = self.skills_home / "skills" / "skiff"
|
||||||
|
project_skill.mkdir()
|
||||||
|
project_skill.joinpath("SKILL.md").write_text(
|
||||||
|
"---\nname: skiff\ndescription: >-\n"
|
||||||
|
" 创建和维护自研 skill。用于创建、安装、反馈或更新 skill 时。\n"
|
||||||
|
"---\n\n# skiff\n\n## 步骤\n\n1. 维护 skill。\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.temp_dir.cleanup()
|
||||||
|
|
||||||
|
def run_skiff(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HOME"] = str(self.home)
|
||||||
|
env["PYTHONPATH"] = str(REPO_ROOT)
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "-m", "skiff", *args],
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
env=env,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_setup_command_is_not_exposed(self) -> None:
|
||||||
|
help_result = self.run_skiff("--help")
|
||||||
|
setup_result = self.run_skiff("setup", str(self.skills_home))
|
||||||
|
|
||||||
|
self.assertEqual(help_result.returncode, 0, help_result.stderr)
|
||||||
|
self.assertNotIn("setup", help_result.stdout)
|
||||||
|
self.assertNotEqual(setup_result.returncode, 0)
|
||||||
|
|
||||||
|
def test_bootstrap_installs_project_skill_globally_for_all_agents(self) -> None:
|
||||||
|
first = self.run_skiff("bootstrap")
|
||||||
|
second = self.run_skiff("bootstrap")
|
||||||
|
|
||||||
|
self.assertEqual(first.returncode, 0, first.stderr)
|
||||||
|
self.assertEqual(second.returncode, 0, second.stderr)
|
||||||
|
expected = (self.skills_home / "skills" / "skiff").resolve()
|
||||||
|
for relative in (
|
||||||
|
".cursor/skills/skiff",
|
||||||
|
".claude/skills/skiff",
|
||||||
|
".codex/skills/skiff",
|
||||||
|
):
|
||||||
|
link = self.home / relative
|
||||||
|
self.assertTrue(link.is_symlink(), relative)
|
||||||
|
self.assertEqual(link.resolve(), expected)
|
||||||
|
self.assertIn("cursor", first.stdout)
|
||||||
|
self.assertIn("claude", first.stdout)
|
||||||
|
self.assertIn("codex", first.stdout)
|
||||||
|
|
||||||
|
def test_create_writes_draft_and_brief_without_registering_owned_skill(self) -> None:
|
||||||
|
project = self.home / "project"
|
||||||
|
project.mkdir()
|
||||||
|
|
||||||
|
result = self.run_skiff(
|
||||||
|
"create",
|
||||||
|
"migration-review",
|
||||||
|
"--idea",
|
||||||
|
"检查数据库迁移",
|
||||||
|
"--from-project",
|
||||||
|
str(project),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
draft = self.skills_home / ".drafts" / "migration-review"
|
||||||
|
self.assertTrue((draft / "SKILL.md").is_file())
|
||||||
|
brief = (draft / "brief.yaml").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("idea: 检查数据库迁移", brief)
|
||||||
|
self.assertIn(f"source_project: {project.resolve()}", brief)
|
||||||
|
self.assertFalse((self.skills_home / "skills" / "migration-review").exists())
|
||||||
|
self.assertIn("请完善 skiff 草稿 migration-review", result.stdout)
|
||||||
|
|
||||||
|
def test_check_rejects_template_placeholders(self) -> None:
|
||||||
|
created = self.run_skiff("create", "migration-review", "--idea", "检查数据库迁移")
|
||||||
|
self.assertEqual(created.returncode, 0, created.stderr)
|
||||||
|
|
||||||
|
result = self.run_skiff("check", "migration-review")
|
||||||
|
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("模板占位", result.stderr)
|
||||||
|
|
||||||
|
def test_finalize_moves_valid_draft_and_removes_brief(self) -> None:
|
||||||
|
created = self.run_skiff("create", "migration-review", "--idea", "检查数据库迁移")
|
||||||
|
self.assertEqual(created.returncode, 0, created.stderr)
|
||||||
|
draft = self.skills_home / ".drafts" / "migration-review"
|
||||||
|
draft.joinpath("SKILL.md").write_text(
|
||||||
|
"---\nname: migration-review\ndescription: >-\n"
|
||||||
|
" 检查数据库迁移文件的安全性。用于用户修改数据库结构、迁移或回滚方案时。\n"
|
||||||
|
"---\n\n# 数据库迁移检查\n\n## 步骤\n\n1. 检查迁移与回滚路径。\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.assertFalse((final / "brief.yaml").exists())
|
||||||
|
self.assertFalse(draft.exists())
|
||||||
|
|
||||||
|
def test_check_rejects_missing_local_reference(self) -> None:
|
||||||
|
draft = self.skills_home / ".drafts" / "reference-check"
|
||||||
|
draft.mkdir(parents=True)
|
||||||
|
draft.joinpath("SKILL.md").write_text(
|
||||||
|
"---\nname: reference-check\ndescription: >-\n"
|
||||||
|
" 检查本地参考资料。用于用户要求核对参考文档时。\n"
|
||||||
|
"---\n\n# 参考检查\n\n读取 [规范](references/rules.md)。\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.run_skiff("check", "reference-check")
|
||||||
|
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("引用文件不存在", 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)
|
||||||
|
final = self.skills_home / "skills" / "collision-check"
|
||||||
|
final.mkdir()
|
||||||
|
final.joinpath("marker").write_text("keep", encoding="utf-8")
|
||||||
|
|
||||||
|
result = self.run_skiff("finalize", "collision-check")
|
||||||
|
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(final.joinpath("marker").read_text(encoding="utf-8"), "keep")
|
||||||
|
self.assertTrue((self.skills_home / ".drafts" / "collision-check" / "brief.yaml").is_file())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class InstallScriptTests(unittest.TestCase):
|
||||||
|
def test_install_bootstraps_skiff_skill_for_all_agents(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
home = Path(temp)
|
||||||
|
(home / ".skills").symlink_to(REPO_ROOT, target_is_directory=True)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HOME"] = str(home)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["bash", str(REPO_ROOT / "install.sh")],
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
env=env,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
for relative in (
|
||||||
|
".cursor/skills/skiff",
|
||||||
|
".claude/skills/skiff",
|
||||||
|
".codex/skills/skiff",
|
||||||
|
):
|
||||||
|
link = home / relative
|
||||||
|
self.assertTrue(link.is_symlink(), relative)
|
||||||
|
self.assertEqual(link.resolve(), (REPO_ROOT / "skills" / "skiff").resolve())
|
||||||
|
self.assertIn("已安装项目 skill", result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectSkillContentTests(unittest.TestCase):
|
||||||
|
def test_skiff_skill_documents_project_usage_and_feedback_loop(self) -> None:
|
||||||
|
content = (REPO_ROOT / "skills" / "skiff" / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for expected in (
|
||||||
|
"skiff add <name>",
|
||||||
|
"问题或优化回流",
|
||||||
|
"实际结果",
|
||||||
|
"期望结果",
|
||||||
|
"skiff check <name>",
|
||||||
|
"~/.skills/skills/<name>/",
|
||||||
|
"第三方 skill",
|
||||||
|
):
|
||||||
|
self.assertIn(expected, content)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user