- hooks/git/{pre-commit,pre-push} + scripts/install-git-hooks.sh(Gitea Actions 无 runner,本地兜底)
- install-skills.sh: --scope project --project-dir(装到项目 .agents/skills)
- scripts/scan-installed.sh: 扫第三方已装技能(33 个,0 高危)
- generate-catalog: has_evals/has_docs + coverage;validate --coverage + 路由一致性检查
- SCOPE 覆盖率路线图(3-4 周补齐 40 个技能的 evals 与四段式文档)
- MAINTENANCE/README/CHANGELOG 同步
This commit is contained in:
@@ -55,9 +55,12 @@ def build() -> dict:
|
||||
fm = frontmatter(skill_md.read_text(encoding="utf-8", errors="replace"))
|
||||
desc = fm.get("description", "")
|
||||
body = skill_md.read_text(encoding="utf-8", errors="replace")
|
||||
install_name = meta.get("install_name") or fm.get("name")
|
||||
has_evals = any((plugin_dir / "skills").rglob("evals/evals.json"))
|
||||
has_docs = (ROOT / "docs" / "skills" / f"{install_name}.md").is_file()
|
||||
skills.append(
|
||||
{
|
||||
"name": meta.get("install_name") or fm.get("name"),
|
||||
"name": install_name,
|
||||
"category": meta.get("dir_category"),
|
||||
"version": meta.get("version"),
|
||||
"status": meta.get("status", "promoted"),
|
||||
@@ -66,14 +69,28 @@ def build() -> dict:
|
||||
"description_len": len(desc),
|
||||
"path": str(skill_md.relative_to(ROOT)),
|
||||
"digest": hashlib.sha256(body.encode("utf-8")).hexdigest()[:16],
|
||||
"has_evals": has_evals,
|
||||
"has_docs": has_docs,
|
||||
}
|
||||
)
|
||||
counts: dict = {}
|
||||
for skill in skills:
|
||||
key = f"{skill['status']}/{skill['invocation']}"
|
||||
counts[key] = counts.get(key, 0) + 1
|
||||
required = [
|
||||
x for x in skills
|
||||
if x["status"] == "promoted" and x["invocation"] == "model-invoked"
|
||||
]
|
||||
coverage = {
|
||||
"required": len(required),
|
||||
"with_evals": sum(1 for x in required if x["has_evals"]),
|
||||
"with_docs": sum(1 for x in required if x["has_docs"]),
|
||||
"missing_evals": [x["name"] for x in required if not x["has_evals"]],
|
||||
"missing_docs": [x["name"] for x in required if not x["has_docs"]],
|
||||
}
|
||||
return {
|
||||
"repo": "ai-proj-helper",
|
||||
"coverage": coverage,
|
||||
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"total": len(skills),
|
||||
"counts": counts,
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# 安装本仓库的 git 门禁钩子(pre-commit / pre-push)。
|
||||
set -euo pipefail
|
||||
root="$(git rev-parse --show-toplevel)"
|
||||
[ -d "$root/hooks/git" ] || { echo "找不到 hooks/git"; exit 1; }
|
||||
mkdir -p "$root/.git/hooks"
|
||||
for h in pre-commit pre-push; do
|
||||
cp "$root/hooks/git/$h" "$root/.git/hooks/$h"
|
||||
chmod +x "$root/.git/hooks/$h"
|
||||
echo "已安装 .git/hooks/$h"
|
||||
done
|
||||
echo "跳过单次检查:SKILL_GATE_SKIP=1 git commit ..."
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# scan-installed.sh — 扫描「已安装但不是本仓库管理」的第三方技能,找出高风险内容。
|
||||
# 用法: ./scripts/scan-installed.sh [--strict] [dir ...]
|
||||
# 默认扫描 ~/.agents/skills 与 $DSH_HOME/skills
|
||||
# --strict: 有高危项则退出码 1
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
STRICT=0
|
||||
dirs=()
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--strict) STRICT=1 ;;
|
||||
*) dirs+=("$a") ;;
|
||||
esac
|
||||
done
|
||||
if [[ ${#dirs[@]} -eq 0 ]]; then
|
||||
dirs+=("${HOME}/.agents/skills")
|
||||
[[ -n "${DSH_HOME:-}" ]] && dirs+=("${DSH_HOME}/skills")
|
||||
fi
|
||||
|
||||
HIGH_PATTERNS=(
|
||||
'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+/($|[[:space:]])'
|
||||
'curl[^|]*\|[[:space:]]*(ba|z)?sh'
|
||||
'wget[^|]*\|[[:space:]]*(ba|z)?sh'
|
||||
'chmod[[:space:]]+777'
|
||||
'BEGIN (RSA|OPENSSH|EC|DSA) PRIVATE KEY'
|
||||
'sk-[A-Za-z0-9]{16,}'
|
||||
'AKIA[0-9A-Z]{16}'
|
||||
'ghp_[A-Za-z0-9]{20,}'
|
||||
)
|
||||
|
||||
managed=""
|
||||
for st in "${HOME}/.agents/.ai-proj-helper-installed-skills.json" \
|
||||
"${DSH_HOME:-/nonexistent}/.ai-proj-helper-installed-skills.json"; do
|
||||
[[ -f "$st" ]] && managed="$managed $(python3 -c "import json;print(' '.join(json.load(open('$st')).keys()))" 2>/dev/null)"
|
||||
done
|
||||
|
||||
total=0 high=0 third=0
|
||||
for dir in "${dirs[@]}"; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
for entry in "$dir"/*; do
|
||||
[[ -e "$entry" || -L "$entry" ]] || continue
|
||||
name="$(basename "$entry")"
|
||||
[[ " $managed " == *" $name "* ]] && continue
|
||||
third=$((third + 1))
|
||||
for pattern in "${HIGH_PATTERNS[@]}"; do
|
||||
while IFS= read -r hit; do
|
||||
echo "[HIGH] $name: ${hit#*:}"
|
||||
high=$((high + 1)); total=$((total + 1))
|
||||
done < <(grep -rnE --include='*.md' --include='*.sh' --include='*.py' --include='*.js' --include='*.ts' --include='*.json' -- "$pattern" "$entry" 2>/dev/null | cut -c1-180)
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "扫描完成:第三方技能 $third 个,高危 $high 处"
|
||||
[[ "$STRICT" == 1 && "$high" -gt 0 ]] && { echo "存在高危第三方技能,需人工确认来源。"; exit 1; }
|
||||
exit 0
|
||||
@@ -136,8 +136,58 @@ def check_skill(rel: Path, skill_md: Path, meta: dict):
|
||||
warnings.append(f"{where}: 引用的相对路径不存在: {target}")
|
||||
|
||||
|
||||
IGNORED_ROUTING_TOKENS = {
|
||||
"completed", "released", "delivery_stage", "status", "true", "false", "user-invoked",
|
||||
"model-invoked", "promoted", "beta", "deprecated", "scoop", "scoop.md",
|
||||
}
|
||||
EXTERNAL_SKILLS = {
|
||||
# 本仓库之外、由 DSH 用户目录提供的技能(docs/skill-routing.md 允许引用)
|
||||
"project-management", "topic-distill", "diary", "dsh-worktable-page",
|
||||
"code-review", "release-notes", "disk-cleanup", "workflow-one",
|
||||
}
|
||||
|
||||
|
||||
def check_routing(catalog_names: set[str]):
|
||||
routing = ROOT / "docs" / "skill-routing.md"
|
||||
if not routing.is_file():
|
||||
return
|
||||
text = routing.read_text(encoding="utf-8")
|
||||
for token in sorted(set(re.findall(r"`([a-z][a-z0-9-]{2,40})`", text))):
|
||||
if token in IGNORED_ROUTING_TOKENS or token in EXTERNAL_SKILLS:
|
||||
continue
|
||||
if token not in catalog_names:
|
||||
warnings.append(f"docs/skill-routing.md: 路由到不存在的技能 {token!r}")
|
||||
|
||||
|
||||
def coverage_report() -> int:
|
||||
missing_evals, missing_docs = [], []
|
||||
for plugin_json in sorted(ROOT.glob("skills-*/*-plugin/.claude-plugin/plugin.json")):
|
||||
meta = json.loads(plugin_json.read_text())
|
||||
if meta.get("status", "promoted") != "promoted":
|
||||
continue
|
||||
skill_md = next((plugin_json.parent.parent / "skills").rglob("SKILL.md"), None)
|
||||
if skill_md is None:
|
||||
continue
|
||||
fm, _ = parse_frontmatter(skill_md.read_text(encoding="utf-8", errors="replace"))
|
||||
if (fm.get("metadata") or {}).get("invocation", "model-invoked") != "model-invoked":
|
||||
continue
|
||||
name = meta.get("install_name") or fm.get("name")
|
||||
if not any((plugin_json.parent.parent / "skills").rglob("evals/evals.json")):
|
||||
missing_evals.append(name)
|
||||
if not (ROOT / "docs" / "skills" / f"{name}.md").is_file():
|
||||
missing_docs.append(name)
|
||||
print(f"覆盖率(promoted + model-invoked):缺 evals {len(missing_evals)} 个,缺四段式文档 {len(missing_docs)} 个")
|
||||
if missing_evals:
|
||||
print("缺 evals:", ", ".join(missing_evals[:20]) + ("..." if len(missing_evals) > 20 else ""))
|
||||
if missing_docs:
|
||||
print("缺文档:", ", ".join(missing_docs[:20]) + ("..." if len(missing_docs) > 20 else ""))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
strict = "--strict" in sys.argv
|
||||
if "--coverage" in sys.argv:
|
||||
return coverage_report()
|
||||
plugins = sorted(ROOT.glob("skills-*/*-plugin/.claude-plugin/plugin.json"))
|
||||
if not plugins:
|
||||
print("未找到任何 plugin.json", file=sys.stderr)
|
||||
@@ -145,6 +195,11 @@ def main() -> int:
|
||||
for plugin_json in plugins:
|
||||
check_plugin(plugin_json)
|
||||
|
||||
catalog = ROOT / "catalog.json"
|
||||
if catalog.is_file():
|
||||
names = {x["name"] for x in json.loads(catalog.read_text()).get("skills", [])}
|
||||
check_routing(names)
|
||||
|
||||
for warning in warnings:
|
||||
print(f"[warn] {warning}")
|
||||
for error in errors:
|
||||
|
||||
Reference in New Issue
Block a user