le-spec v1.0.0:Le SPEC 智能需求分析与 SPEC 生成总控 skill——真实集成 grilling/grill-with-docs(domain-modeling) + think + openspec(CLI 1.16.1 + 7 个 openspec-* skills,均经 skills.sh 装入 ~/.agents/skills 并软链四 runtime);七阶段工作流(识别/十类分拣/Grill 访谈/Think 挑战/反向验收/OpenSpec 生成/四视角审查)复杂度三档自适应;状态 state.md + 调用账本 ledger.jsonl 双持久化支撑中断恢复与真实调用核验;依赖检查 deps_check.py 分级报缺+精确安装命令,ledger.py append/check/tail;四类回合终止停点(批准前不编码为硬约束);测试 A-I 九项全过:两场景 fresh-context 子代理多轮实测(4 轮访谈+变更+中断恢复+strict validate 双项目通过),账本 28 条真实调用(grilling×9/think×2/openspec CLI×14),独立子代理审查实测抓出自审遗漏的 2 blocking;已知限制 7 项如实入库(reports/test-report.md)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Le SPEC 依赖检查:skill 目录 + openspec CLI。
|
||||
|
||||
用法:
|
||||
python3 deps_check.py # 人读输出, 退出码 0=required 全就位, 1=有缺失
|
||||
python3 deps_check.py --json # 机器可读
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 依赖清单: (name, level, source, install_cmd)
|
||||
SKILL_DEPS = [
|
||||
("grilling", "required", "mattpocock/skills", "npx skills@latest add mattpocock/skills --skill grilling grill-me grill-with-docs domain-modeling -g"),
|
||||
("domain-modeling", "recommended", "mattpocock/skills", "npx skills@latest add mattpocock/skills --skill domain-modeling -g"),
|
||||
("grill-me", "optional", "mattpocock/skills", None), # 入口薄壳
|
||||
("grill-with-docs", "optional", "mattpocock/skills", None), # 入口薄壳
|
||||
("think", "required", "tw93/Waza", "npx skills@latest add tw93/Waza --skill think -g"),
|
||||
("openspec-propose", "recommended", "Fission-AI/OpenSpec", "npx skills@latest add Fission-AI/OpenSpec --skill openspec-propose openspec-new-change openspec-update-change openspec-verify-change openspec-archive-change openspec-apply-change openspec-explore -g"),
|
||||
("openspec-new-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
("openspec-update-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
("openspec-verify-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
("openspec-archive-change", "recommended", "Fission-AI/OpenSpec", None),
|
||||
]
|
||||
|
||||
# 候选 skill 根目录(顺序即优先级); 可用 LESPEC_SKILL_PATHS 覆盖, 冒号分隔
|
||||
DEFAULT_ROOTS = [
|
||||
"~/.agents/skills",
|
||||
"~/.zcode/skills",
|
||||
"~/.claude/skills",
|
||||
"~/.codex/skills",
|
||||
]
|
||||
|
||||
|
||||
def skill_roots():
|
||||
env = os.environ.get("LESPEC_SKILL_PATHS")
|
||||
if env:
|
||||
return [Path(p).expanduser() for p in env.split(":") if p.strip()]
|
||||
return [Path(p).expanduser() for p in DEFAULT_ROOTS]
|
||||
|
||||
|
||||
def find_skill(name):
|
||||
"""返回 (resolved_skill_md_path | None, root_dir)。软链解析到真源。"""
|
||||
for root in skill_roots():
|
||||
cand = root / name / "SKILL.md"
|
||||
if cand.exists():
|
||||
try:
|
||||
return cand.resolve(), root
|
||||
except OSError:
|
||||
return cand, root
|
||||
return None, None
|
||||
|
||||
|
||||
def check_openspec_cli():
|
||||
exe = shutil.which("openspec")
|
||||
if not exe:
|
||||
return {"name": "openspec-cli", "level": "required", "ok": False,
|
||||
"detail": "openspec not in PATH",
|
||||
"install": "npm install -g @fission-ai/openspec@latest"}
|
||||
try:
|
||||
out = subprocess.run([exe, "--version"], capture_output=True, text=True,
|
||||
timeout=15)
|
||||
ver = (out.stdout or out.returncode and out.stderr or "").strip().splitlines()[-1] \
|
||||
if (out.stdout or out.stderr) else "?"
|
||||
return {"name": "openspec-cli", "level": "required", "ok": out.returncode == 0,
|
||||
"detail": f"{exe} (v{ver})",
|
||||
"install": "npm install -g @fission-ai/openspec@latest"}
|
||||
except Exception as e: # timeout / spawn 失败
|
||||
return {"name": "openspec-cli", "level": "required", "ok": False,
|
||||
"detail": f"exec failed: {e}",
|
||||
"install": "npm install -g @fission-ai/openspec@latest"}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--json", action="store_true", help="JSON 输出")
|
||||
args = ap.parse_args()
|
||||
|
||||
results = []
|
||||
for name, level, source, install in SKILL_DEPS:
|
||||
path, root = find_skill(name)
|
||||
results.append({
|
||||
"name": name, "kind": "skill", "level": level,
|
||||
"ok": path is not None,
|
||||
"detail": str(path) if path else f"not found under {':'.join(str(r) for r in skill_roots())}",
|
||||
"source": source, "install": install,
|
||||
})
|
||||
results.append(check_openspec_cli())
|
||||
|
||||
missing_required = [r for r in results if not r["ok"] and r["level"] == "required"]
|
||||
missing_recommended = [r for r in results if not r["ok"] and r["level"] == "recommended"]
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"ok": not missing_required,
|
||||
"missing_required": [r["name"] for r in missing_required],
|
||||
"missing_recommended": [r["name"] for r in missing_recommended],
|
||||
"items": results,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
mark = lambda r: "✅" if r["ok"] else ("❌" if r["level"] == "required" else "⚠️ ")
|
||||
print(f"{'':2}{'状态':4} {'级别':12} {'依赖':24} 详情")
|
||||
for r in results:
|
||||
print(f"{mark(r)} {r['level']:<12} {r['name']:<24} {r['detail']}")
|
||||
print()
|
||||
if not missing_required and not missing_recommended:
|
||||
print("全部依赖就位。")
|
||||
for r in missing_required + missing_recommended:
|
||||
if r.get("install"):
|
||||
print(f"补装 {r['name']}: {r['install']}")
|
||||
print("提示: optional 级(grill-me/grill-with-docs)是入口薄壳, 缺失不影响运行。")
|
||||
|
||||
sys.exit(1 if missing_required else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user