feat: add skill draft workflow

This commit is contained in:
2026-07-27 14:03:19 +08:00
parent 8b82539f6f
commit 86cd1fa36d
14 changed files with 535 additions and 100 deletions
+91 -32
View File
@@ -6,6 +6,7 @@ import argparse
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
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.paths import (
ALL_TARGETS,
DRAFTS_DIR,
EXTERNALS_DIR,
SKILLS_DIR,
SKILLS_HOME,
@@ -34,8 +36,10 @@ from skiff.skills import (
owned_skill_path,
resolve_skill_source,
skill_description,
validate_skill_dir,
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
@@ -130,29 +134,6 @@ def _remove_skill(
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:
ensure_skills_home()
owned = list_owned_skills()
@@ -171,6 +152,15 @@ def cmd_list(args: argparse.Namespace) -> None:
_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]]:
skill_path, _ = resolve_skill_source(name)
rows: list[tuple[str, Path, Path]] = []
@@ -386,7 +376,7 @@ def cmd_create(args: argparse.Namespace) -> None:
if not TEMPLATE_DIR.is_dir():
raise SystemExit(f"模板目录不存在: {TEMPLATE_DIR}")
dst = SKILLS_DIR / args.name
dst = DRAFTS_DIR / args.name
copy_template(TEMPLATE_DIR, dst)
skill_md = dst / "SKILL.md"
@@ -399,10 +389,67 @@ def cmd_create(args: argparse.Namespace) -> None:
flags=re.MULTILINE,
)
skill_md.write_text(content, encoding="utf-8")
_print(f"已创建 skill: {dst}")
_print(f"下一步: 编辑 {skill_md}")
_print(f" skiff publish skills/{args.name} -m \"add {args.name}\" --push")
_print(f" skiff add {args.name} -a cursor -g -y")
brief = {
"name": args.name,
"idea": args.idea or "",
"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:
@@ -483,9 +530,11 @@ def build_parser() -> argparse.ArgumentParser:
sub = parser.add_subparsers(dest="command", required=True)
p_setup = sub.add_parser("setup", help="关联 ~/.skills 到 skills 仓库")
p_setup.add_argument("path", nargs="?", default=str(SKILLS_HOME), help="仓库路径(默认 ~/.skills")
p_setup.set_defaults(func=cmd_setup)
p_bootstrap = sub.add_parser(
"bootstrap",
help="将本项目 skiff skill 全局安装到所有 agent",
)
p_bootstrap.set_defaults(func=cmd_bootstrap)
p_list = sub.add_parser("list", help="列出 ~/.skills 中的 skill 目录")
p_list.set_defaults(func=cmd_list)
@@ -557,10 +606,20 @@ def build_parser() -> argparse.ArgumentParser:
p_sync.add_argument("--project")
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("--idea", help="创建 skill 的原始想法")
p_create.add_argument("--from-project", help="想法来源项目(仅记录上下文)")
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.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
p_doctor.add_argument("--fix", action="store_true", help="自动修复可修复的软链")