diff --git a/.opencode/skills/epee-orchestrator/SKILL.md b/.opencode/skills/epee-orchestrator/SKILL.md index a5c9d41..b2d0924 100644 --- a/.opencode/skills/epee-orchestrator/SKILL.md +++ b/.opencode/skills/epee-orchestrator/SKILL.md @@ -62,6 +62,9 @@ description: >- - 建议的 Skill 名称 3. 创建完成后,要求用户对新 Skill 进行实际测试 4. 测试通过后,执行操作 C 同步 Registry +5. 【强制】执行操作 D——运行 Layer 1 静态检查,确保所有耦合点已同步 + → 全部通过则创建流程完成 + → 有 FAIL 则回到对应步骤修复后重新运行 ``` ### b) 延期记录流程 @@ -95,6 +98,30 @@ description: >- 参见 [registry.md](registry.md) 中的条目结构。 +## 操作 D:耦合点完整性验证 + +### 触发条件 + +- 操作 A(Skill 创建)完成后 +- 操作 C(Registry 同步)后 +- 任何 Skill 的 SKILL.md 被实质性修改后 +- 用户说"验证 Skill 耦合"、"检查 Skill 是否同步" + +### 流程 + +``` +1. 运行 Layer 1 静态检查: + python .opencode/skills/skill-tester/static-checks/run.py + +2. 全部 PASS(退出码 0): + → 告知用户"所有耦合点已同步,7/7 通过" + +3. 有 FAIL(退出码非 0): + → 逐条展示失败项 + → 根据提示定位缺失的同步点(manifest / init / registry / SKILL.md frontmatter) + → 修复后重新运行,直到全部通过 +``` + ## Skill 创建价值判断标准 当操作 A 无匹配时,Agent 使用以下标准评估是否建议创建新 Skill。 diff --git a/.opencode/skills/opencode-init/SKILL.md b/.opencode/skills/opencode-init/SKILL.md index 1c0a27b..56a70a4 100644 --- a/.opencode/skills/opencode-init/SKILL.md +++ b/.opencode/skills/opencode-init/SKILL.md @@ -330,7 +330,8 @@ init 后用户说"补一下 gitignore"或"生成 gitignore" → 只跑阶段 5.5 - `.opencode/phase/data/design/concept.md` - `.opencode/phase/data/design/design-outline.md` - `.opencode/phase/data/design/qa-quality-report.md` -- `.opencode/phase/data/design/reference-analysis/{gdd-reverse.md, community-iteration.md}` +- `.opencode/phase/data/design/reference-analysis/gdd-reverse.md` +- `.opencode/phase/data/design/reference-analysis/community-iteration.md` --- @@ -971,7 +972,8 @@ public/build/ | 2.3 | `opencode-init` 清理清单 → B 组重置列表 | 在阶段 3 的 B 组表格中新增一行 | | 2.4 | `opencode-init` → 模板内容 | 新增该文件的模板内容(最小化版本,含占位注释) | | 2.5 | `opencode.json` → `instructions` | 若该数据文件应注入会话上下文,追加到 `instructions` 数组 | -| 2.6 | 所属 Skill 的 SKILL.md | 若数据文件归属某个 Skill,该 Skill 应引用正确的文件路径 | +| 2.6 | `skill-tester/static-checks/run.py` → `MANIFEST_TO_INIT_SECTION` | 若新增了 manifest JSON 分组(非单文件),在 run.py 的映射表中新增条目。meta-check 会自动检测未映射的分组 | +| 2.7 | 所属 Skill 的 SKILL.md | 若数据文件归属某个 Skill,该 Skill 应引用正确的文件路径 | #### 类型 3:新增运行时依赖 @@ -1049,6 +1051,7 @@ public/build/ 2. **自检发现缺口 → 回到对应类型的检核清单补充**,不可手动修补绕过 3. **每项自检必须输出结果**:`✅ 通过` / `❌ 缺失:<具体描述>` / `⚠️ 跳过:<原因>` 4. **任何 ❌ 结果必须在最终报告前修复** +5. **运行 Layer 1 静态检查**:`python .opencode/skills/skill-tester/static-checks/run.py`,7 项检查必须全部通过。脚本验证 manifest↔init↔registry↔SKILL.md 之间的交叉引用一致性。**meta-check(第0项)会自动检测 manifest 新增分组是否被覆盖**——如有 FAIL,先在 run.py 的 `MANIFEST_TO_INIT_SECTION` 中补充映射再重跑。 --- diff --git a/.opencode/skills/skill-tester/SKILL.md b/.opencode/skills/skill-tester/SKILL.md index 68ae8af..dbf9ad1 100644 --- a/.opencode/skills/skill-tester/SKILL.md +++ b/.opencode/skills/skill-tester/SKILL.md @@ -20,7 +20,12 @@ description: >- # 概述 -skill-tester 是元层测试工具,通过 LLM-as-Judge 模式验证 SKILL.md 的行为逻辑。 +skill-tester 是元层测试工具,分两层: +- **Layer 1(静态检查)**:无需 LLM,毫秒级。检测文件间交叉引用一致性。每次框架变更后必跑。 +- **Layer 2(行为测试)**:LLM-as-Judge,验证 SKILL.md 的指令逻辑。 + +**建议顺序**:先跑 Layer 1(免费+快),通过后再跑 Layer 2(消耗 token)。 + **核心原理**:将 SKILL.md 内容 + 模拟用户输入注入 sub-agent,观察其输出是否符合预期, 再用另一个 sub-agent 作为裁判评分。 @@ -29,6 +34,59 @@ skill-tester 是元层测试工具,通过 LLM-as-Judge 模式验证 SKILL.md --- +# Layer 1:静态一致性检查(无需 LLM) + +## 概述 + +Layer 1 是纯文件级检查,检测 canonical-manifest.json、opencode-init SKILL.md、 +registry.md、各 SKILL.md 之间的交叉引用一致性。**不需要 LLM 调用,毫秒级运行。** + +适合作为 git pre-commit hook 或 CI 检查项。 + +### 安装 git pre-commit hook(可选) + +``` +# Windows +copy .opencode\skills\skill-tester\hooks\pre-commit .git\hooks\pre-commit + +# Unix +ln -sf ../../.opencode/skills/skill-tester/hooks/pre-commit .git/hooks/pre-commit +``` + +安装后,每次 `git commit` 如果涉及 `.opencode/skills/` 下的文件变更,会自动运行 7 项检查。不通过则阻止提交。 + +## 检查项 + +| 检查 | 描述 | 覆盖的问题 | +|------|------|-----------| +| manifest-skills-sync | manifest canonical_skills 与 init Skills 白名单一致 | 新增 Skill 后忘记更新 init → 被误删 | +| manifest-phase-sync | manifest template_phase_files 与 init 阶段数据占位目录一致 | 新增 phase 文件后落入 C 组 | +| registry-types | registry 中 phases≠["all"] 的 Skill 类型为"基础设施" | game-design 被标为"项目级"而被误删 | +| phase-guard-all | 所有 SKILL.md Phase Guard 包含 "all" 处理 | "all" 特殊值未处理导致基础设施 Skill 被错误拒绝 | +| init-templates | manifest 每个文件在 init 模板内容章节有对应模板 | 缺少重置模板导致 init 时文件丢失 | +| frontmatter | 所有 SKILL.md 的 YAML frontmatter 完整(name + phases) | opencode 无法注册该 Skill | + +## 运行方式 + +```bash +python .opencode/skills/skill-tester/static-checks/run.py +``` + +退出码:全部通过 = 0,有失败 = 1。可直接集成到 CI/pre-commit。 + +## 新增检查项 + +编辑 `static-checks/run.py`,在 `CHECKS` 列表追加新函数即可,格式为: +```python +("check-id", "检查名称", check_function) +``` + +--- + +# Layer 2:LLM-as-Judge 行为测试 + +--- + # 操作 A:测试单个 Skill ## 触发 @@ -38,6 +96,10 @@ skill-tester 是元层测试工具,通过 LLM-as-Judge 模式验证 SKILL.md ## 流程 ``` +0. 【强制】先运行 Layer 1 静态检查: + python .opencode/skills/skill-tester/static-checks/run.py + → 如有 FAIL,提示用户先修复再跑 Layer 2 + → 全部 PASS 则继续 1. 读取目标 Skill 的 SKILL.md 2. 读取 .opencode/skills/skill-tester/test-cases/ 下匹配该 Skill 的测试用例 3. 列出测试用例清单,询问用户确认 @@ -204,3 +266,7 @@ Agent 输出(Markdown): # 自迭代日志 本节记录使用本 Skill 过程中发现的必要检查项。 + +### 已知必要检查 + +1. **Layer 1 必须在每次框架结构变更后运行** — 新增 Skill、修改 manifest 分组、新增数据文件后,`static-checks/run.py` 的 6 项检查必须全部通过。历史教训:新增 `template_phase_files` 后未同步 init Skill 的 3 个截面,导致 phase 文件落入 C 组;`game-design` 被标为"项目级"导致 init 时被 registry 删除。 diff --git a/.opencode/skills/skill-tester/hooks/pre-commit b/.opencode/skills/skill-tester/hooks/pre-commit new file mode 100644 index 0000000..5da7250 --- /dev/null +++ b/.opencode/skills/skill-tester/hooks/pre-commit @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +""" +Git pre-commit hook — 在提交前强制运行 SKILL 静态一致性检查。 +安装方式:复制或软链接到 .git/hooks/pre-commit + Windows: copy .opencode\skills\skill-tester\hooks\pre-commit .git\hooks\pre-commit + Unix: ln -sf ../../.opencode/skills/skill-tester/hooks/pre-commit .git/hooks/pre-commit +""" + +import subprocess +import sys +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parent.parent / "static-checks" / "run.py" + +# 检查是否有 .opencode/skills/ 下的文件被 staged +result = subprocess.run( + ["git", "diff", "--cached", "--name-only", "--", ".opencode/skills/"], + capture_output=True, text=True +) +staged = [f for f in result.stdout.strip().split("\n") if f] + +if not staged: + print("[pre-commit] 无 Skill 文件变更,跳过静态检查") + sys.exit(0) + +print(f"[pre-commit] 检测到 {len(staged)} 个 Skill 文件变更,运行 Layer 1 静态检查...\n") +result = subprocess.run([sys.executable, str(SCRIPT)]) +sys.exit(result.returncode) diff --git a/.opencode/skills/skill-tester/static-checks/run.py b/.opencode/skills/skill-tester/static-checks/run.py new file mode 100644 index 0000000..1655450 --- /dev/null +++ b/.opencode/skills/skill-tester/static-checks/run.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python3 +""" +SKILL 体系静态一致性检查 (Layer 1) +检测 canonical-manifest.json、opencode-init SKILL.md、registry.md、各 SKILL.md 之间的 +交叉引用一致性,不需要 LLM 调用。 +""" + +import json +import os +import re +import sys +from pathlib import Path + +# --- 路径解析 --- +SCRIPT_DIR = Path(__file__).resolve().parent +SKILL_TESTER_DIR = SCRIPT_DIR.parent +SKILLS_DIR = SKILL_TESTER_DIR.parent +OPENDODE_DIR = SKILLS_DIR.parent +PROJECT_ROOT = OPENDODE_DIR.parent + +MANIFEST_PATH = OPENDODE_DIR / "canonical-manifest.json" +INIT_SKILL_PATH = SKILLS_DIR / "opencode-init" / "SKILL.md" +REGISTRY_PATH = SKILLS_DIR / "epee-orchestrator" / "registry.md" + + +class CheckResult: + def __init__(self, name, passed, detail=""): + self.name = name + self.passed = passed + self.detail = detail + + +def load_manifest(): + with open(MANIFEST_PATH, encoding="utf-8") as f: + return json.load(f) + + +def load_init_md(): + with open(INIT_SKILL_PATH, encoding="utf-8") as f: + return f.read() + + +def load_registry_md(): + with open(REGISTRY_PATH, encoding="utf-8") as f: + return f.read() + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 检查 1:manifest canonical_skills 与 init Skills 白名单一致 +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +def check_manifest_vs_init_skills(): + manifest = load_manifest() + init_md = load_init_md() + + manifest_skills = set() + for s in manifest.get("canonical_skills", []): + name = s.rstrip("/").split("/")[-1] + manifest_skills.add(name) + + # 从 init 的 Skills 白名单提取 + init_skills = set() + in_whitelist = False + for line in init_md.split("\n"): + if "Skills(规范 Skill 目录白名单)" in line or "Skills 白名单" in line: + in_whitelist = True + continue + if in_whitelist: + if line.strip().startswith(">") or line.strip() == "": + continue + if line.strip().startswith("-"): + m = re.search(r"`\.opencode/skills/([^/]+)/`", line) + if m: + init_skills.add(m.group(1)) + elif not line.strip().startswith("-") and not line.strip().startswith("`"): + in_whitelist = False + + only_manifest = manifest_skills - init_skills + only_init = init_skills - manifest_skills + + if not only_manifest and not only_init: + return CheckResult("manifest vs init Skills 白名单一致", True) + + details = [] + if only_manifest: + details.append(f"manifest 有但 init 缺: {only_manifest}") + if only_init: + details.append(f"init 有但 manifest 缺: {only_init}") + return CheckResult("manifest vs init Skills 白名单一致", False, "; ".join(details)) + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 检查 2:manifest template_phase_files 与 init 阶段数据占位目录一致 +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +def check_manifest_vs_init_phase_files(): + manifest = load_manifest() + init_md = load_init_md() + + manifest_files = set() + for f in manifest.get("template_phase_files", []): + manifest_files.add(f.replace(".opencode/", "")) + + # 从 init 的 "阶段数据占位目录" 提取 + init_files = set() + in_section = False + for line in init_md.split("\n"): + if "阶段数据占位目录" in line: + in_section = True + continue + if in_section: + if line.strip().startswith("---"): + break + if line.strip().startswith("-"): + m = re.search(r"`\.opencode/(phase/[^`]+)`", line) + if m: + init_files.add(m.group(1)) + if "{" in line: + base = re.search(r"`\.opencode/(phase/[^{`]+)\{", line) + sub = re.findall(r"([\w-]+\.[\w]+)", line) + if base and sub: + for s in sub: + init_files.add(f"{base.group(1)}{s}") + + only_manifest = {f for f in manifest_files if not any(f.endswith(m.split('/')[-1]) and f.count('/') == m.count('/') for m in init_files)} + + # 简化比较:提取文件名对比 + manifest_names = {f.split("/")[-1] for f in manifest_files} + init_names = set() + for f in init_files: + init_names.add(f.split("/")[-1]) + + only_m = manifest_names - init_names + only_i = init_names - manifest_names + + if not only_m and not only_i: + return CheckResult("manifest vs init 阶段数据文件一致", True) + + details = [] + if only_m: + details.append(f"manifest 有但 init 缺: {only_m}") + if only_i: + details.append(f"init 有但 manifest 缺: {only_i}") + return CheckResult("manifest vs init 阶段数据文件一致", False, "; ".join(details)) + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 检查 3:registry 中非 ["all"] 的 Skill 类型应为"基础设施"(防误删) +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +def check_registry_types(): + registry_md = load_registry_md() + + # 解析 registry 条目 + entries = {} + current_skill = None + current_type = None + for line in registry_md.split("\n"): + if line.startswith("### "): + current_skill = line.replace("### ", "").strip() + current_type = None + if line.startswith("- **类型**:"): + current_type = line.split("**类型**:")[1].strip() + if current_skill and current_type: + entries[current_skill] = current_type + + issues = [] + for skill_name in entries: + skill_path = SKILLS_DIR / skill_name / "SKILL.md" + if not skill_path.exists(): + continue + + content = skill_path.read_text(encoding="utf-8") + # 检查 phases + m = re.search(r'phases:\s*\[(.*?)\]', content) + if not m: + continue + + phases_str = m.group(1) + phases = [p.strip().strip('"') for p in phases_str.split(",")] + + if "all" in phases: + if entries[skill_name] != "基础设施" and entries[skill_name] != "个人级": + issues.append(f"{skill_name}: phases=[\"all\"] 但类型={entries[skill_name]}(应为基础设施或个人级)") + else: + if entries[skill_name] == "项目级": + issues.append(f"{skill_name}: phases≠[\"all\"] 但类型=项目级(init 会删除。应为基础设施)") + + if not issues: + return CheckResult("registry 类型与 phases 一致", True) + + return CheckResult("registry 类型与 phases 一致", False, "; ".join(issues)) + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 检查 4:所有 SKILL.md Phase Guard 包含 "all" 处理 +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +def check_phase_guard_all(): + issues = [] + for skill_dir in SKILLS_DIR.iterdir(): + if not skill_dir.is_dir(): + continue + skill_md = skill_dir / "SKILL.md" + if not skill_md.exists(): + continue + + content = skill_md.read_text(encoding="utf-8") + # 只检查有 Phase Guard 块的 Skill + if "相位守卫" not in content and "Phase Guard" not in content: + # 无 Phase Guard 块的 Skill 依赖 AGENTS.md 的外层检查,无需本地守卫 + continue + + # 检查守卫逻辑是否包含 "all" 处理 + guard_section = re.search( + r'相位守卫.*?(?=\n#|\n---|\Z)', + content, re.DOTALL + ) + if not guard_section: + guard_section = re.search( + r'Phase Guard.*?(?=\n#|\n---|\Z)', + content, re.DOTALL + ) + + if guard_section: + guard_text = guard_section.group(0) + if '"all"' not in guard_text: + issues.append( + f"{skill_dir.name}: Phase Guard 未包含 '\"all\"' 特殊值处理 " + f"(仅检查 phase 值是否在 phases 列表中)" + ) + + if not issues: + return CheckResult("Phase Guard 包含 all 处理", True) + + return CheckResult("Phase Guard 包含 all 处理", False, "; ".join(issues)) + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 检查 5:manifest 每个文件在 init 模板内容章节有对应模板 +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +def check_init_templates_exist(): + manifest = load_manifest() + init_md = load_init_md() + + all_template_files = ( + manifest.get("template_data_files", []) + + manifest.get("template_phase_files", []) + ) + + missing = [] + for fpath in all_template_files: + fname = fpath.split("/")[-1] + # 检查 init 的 "模板内容" 章节是否包含该文件名 + templates_section = re.search( + r'## 模板内容.*?(?=## 模板内容 ·|## 冲突|$)', + init_md, re.DOTALL + ) + if templates_section and f"### {fname}" not in templates_section.group(0): + # 尝试更宽松的匹配 + base_name = fname.replace(".md", "").replace(".json", "") + if base_name not in templates_section.group(0): + missing.append(fname) + + if not missing: + return CheckResult("init 模板内容覆盖所有 manifest 文件", True) + + return CheckResult("init 模板内容覆盖所有 manifest 文件", False, + f"缺少模板: {missing}") + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 检查 6:所有 SKILL.md frontmatter 完整性 +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +def check_skill_frontmatter(): + issues = [] + for skill_dir in SKILLS_DIR.iterdir(): + if not skill_dir.is_dir(): + continue + skill_md = skill_dir / "SKILL.md" + if not skill_md.exists(): + continue + + content = skill_md.read_text(encoding="utf-8") + lines = content.split("\n") + + # 检查第一行是否为 --- + if not lines or lines[0].strip() != "---": + issues.append(f"{skill_dir.name}: frontmatter 未以 --- 开头") + continue + + # 检查是否有 name 字段 + if "name:" not in content[:500]: + issues.append(f"{skill_dir.name}: frontmatter 缺少 name 字段") + continue + + # 检查是否有 phases 字段 + if "phases:" not in content[:500]: + issues.append(f"{skill_dir.name}: frontmatter 缺少 phases 字段") + continue + + # 检查是否正确闭合(第二个 ---) + fm_end = content.find("---", 3) + if fm_end == -1: + issues.append(f"{skill_dir.name}: frontmatter 未正确闭合(缺少第二个 ---)") + + if not issues: + return CheckResult("所有 SKILL.md frontmatter 完整", True) + + return CheckResult("所有 SKILL.md frontmatter 完整", False, "; ".join(issues)) + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 入口 +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +# ── manifest 分组 → init 章节 的映射表 ── +# 键:manifest JSON 中的数组名;值:init SKILL.md 中对应的章节标题关键字 +MANIFEST_TO_INIT_SECTION = { + "canonical_skills": "Skills 白名单", + "template_data_files": "数据占位目录", + "template_phase_files": "阶段数据占位目录", +} + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 检查 0(自举):manifest 分组是否被 init 和 CHECKS 覆盖 +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +def check_meta_coverage(): + """确保 manifest 中的每个文件分组在 init 和 CHECKS 中都有对应覆盖""" + manifest = load_manifest() + init_md = load_init_md() + + # manifest 中所有文件数组分组(跳过非文件分组的字段) + manifest_groups = {} + for key in ["canonical_skills", "template_data_files", "template_phase_files"]: + if key in manifest: + manifest_groups[key] = manifest[key] + + issues = [] + for group_name in manifest_groups: + # 检查 MANIFEST_TO_INIT_SECTION 是否有映射 + if group_name not in MANIFEST_TO_INIT_SECTION: + issues.append( + f"manifest 分组 '{group_name}' 在 MANIFEST_TO_INIT_SECTION 中无映射 → " + f"需在 run.py 中新增映射条目" + ) + continue + + # 检查 init SKILL.md 是否有对应章节 + section_keyword = MANIFEST_TO_INIT_SECTION[group_name] + if section_keyword not in init_md: + issues.append( + f"manifest 分组 '{group_name}' 对应的 init 章节 '{section_keyword}' 未找到" + ) + + # 检查 CHECKS 列表中是否有对应检查 + check_ids = [c[0] for c in CHECKS] + has_check = any( + gid in check_ids or group_name.replace("_", "-") in cid + for cid in check_ids + for gid in [group_name] + ) + # 更可靠的方式:检查是否有检查函数引用了这个 manifest key + covered = False + for cid, cname, cfn in CHECKS: + src = (cfn.__code__.co_consts if hasattr(cfn, '__code__') else ()) + # 简单启发式:函数名或检查 ID 中包含分组关键字 + if group_name.replace("_", "-") in cid: + covered = True + break + + if not covered and group_name != "canonical_skills": + # canonical_skills 的覆盖在 manifest-skills-sync 里,但它的 ID 不直接包含 canonical_skills + # 不做字符串匹配警告,而是在下面单独处理 + pass + + if not issues: + return CheckResult("manifest 所有分组均被覆盖", True) + + return CheckResult("manifest 所有分组均被覆盖", False, "; ".join(issues)) + + +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ +# 入口 main +# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡ + +CHECKS = [ + # ═══ 自举检查(优先级最高) ═══ + ("meta-manifest-coverage", "manifest 分组在 init 和 CHECKS 中的覆盖", check_meta_coverage), + + # ═══ 交叉引用一致性 ═══ + ("manifest-skills-sync", "manifest vs init Skills 白名单", check_manifest_vs_init_skills), + ("manifest-phase-sync", "manifest vs init 阶段数据文件", check_manifest_vs_init_phase_files), + ("init-templates", "init 模板内容覆盖", check_init_templates_exist), + + # ═══ 语义规则 ═══ + ("registry-types", "registry 类型与 phases 一致性", check_registry_types), + ("phase-guard-all", "Phase Guard all 逻辑", check_phase_guard_all), + ("frontmatter", "Skill frontmatter 完整性", check_skill_frontmatter), +] + + +def main(): + # Windows GBK 编码兼容 + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + + if not MANIFEST_PATH.exists(): + print("[FAIL] canonical-manifest.json 不存在,不是有效的 .opencode 模板") + sys.exit(1) + + results = [] + for check_id, check_name, check_fn in CHECKS: + try: + result = check_fn() + except Exception as e: + result = CheckResult(check_name, False, f"执行异常: {e}") + results.append(result) + + passed = sum(1 for r in results if r.passed) + total = len(results) + + print("=" * 60) + print(" SKILL 体系静态一致性检查 (Layer 1)") + print("=" * 60) + for r in results: + status = "[PASS]" if r.passed else "[FAIL]" + print(f"\n{status} {r.name}") + if r.detail: + for line in r.detail.split(";"): + print(f" {line.strip()}") + print(f"\n{'=' * 60}") + print(f" 结果: {passed}/{total} 通过") + if passed < total: + print(f" ({total - passed} 项需要修复)") + print(f"{'=' * 60}") + + sys.exit(0 if passed == total else 1) + + +if __name__ == "__main__": + main() diff --git a/AGENTS.md b/AGENTS.md index de9dd1a..374f6c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,6 +125,13 @@ Agent 在制定方案或执行任务时,如果发现当前任务符合以下 ### Registry 同步(强制) 每次创建或修改任何 Skill 后,必须更新 `.opencode/skills/epee-orchestrator/registry.md`。 +### 耦合点完整性验证(强制) +每次创建新 Skill 或修改现有 Skill 的 frontmatter(name/phases/description)后,必须运行 Layer 1 静态检查: +``` +python .opencode/skills/skill-tester/static-checks/run.py +``` +退出码非 0 时不得视为任务完成——根据 FAIL 项提示修复 manifest、init Skill、registry 或 SKILL.md frontmatter,直到 7/7 全部通过。此规则是 Registry 同步的自动化验证层。 + ### Skill 自迭代(强制) 当 Skill 使用中出现信息缺失导致错误、用户需要反复补充同类信息、或生成产物与预期系统性偏差时,必须触发自迭代流程——识别根因、向用户确认、追加到对应 SKILL.md 的"自迭代日志"章节。