feat: add visual skill overview reports

This commit is contained in:
yaojingang
2026-04-06 11:07:10 +08:00
parent d3be94fb9b
commit cdcee4a748
12 changed files with 676 additions and 16 deletions
+2
View File
@@ -5,4 +5,6 @@ __pycache__/
tests/tmp/
tests/tmp_snapshot/
tests/tmp_cli/
tests/tmp_skill_overview/
tests/tmp_skill_preview/
reports/release_snapshots/
+5 -2
View File
@@ -1,6 +1,6 @@
PYTHON ?= python3
.PHONY: eval eval-suite route-scorecard route-confusion-check description-optimization judge-blind-eval description-optimization-check promotion-check yao-cli-check description-drift-history iteration-ledger results-panel regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check test clean
.PHONY: eval eval-suite route-scorecard route-confusion-check description-optimization judge-blind-eval description-optimization-check promotion-check yao-cli-check skill-overview-check description-drift-history iteration-ledger results-panel regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check test clean
eval:
$(PYTHON) scripts/trigger_eval.py --description-file evals/improved_description.txt --cases evals/trigger_cases.json --baseline-description-file evals/baseline_description.txt
@@ -29,6 +29,9 @@ promotion-check:
yao-cli-check:
$(PYTHON) tests/verify_yao_cli.py
skill-overview-check:
$(PYTHON) tests/verify_skill_overview.py
description-drift-history:
$(PYTHON) scripts/render_description_drift_history.py
@@ -77,7 +80,7 @@ resource-boundary-check:
quality-check:
$(PYTHON) tests/verify_quality_checks.py
test: eval eval-suite route-scorecard route-confusion-check description-optimization description-optimization-check promotion-check yao-cli-check description-drift-history iteration-ledger regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check
test: eval eval-suite route-scorecard route-confusion-check description-optimization description-optimization-check promotion-check yao-cli-check skill-overview-check description-drift-history iteration-ledger regression-history context-reports portability-report portability-check failure-regression-check package-check package-failure-check snapshot-check validate lint governance-check resource-boundary-check quality-check
clean:
rm -rf dist tests/tmp tests/tmp_snapshot
+3 -2
View File
@@ -19,6 +19,7 @@ It turns rough workflows, transcripts, prompts, notes, and runbooks into reusabl
- a clear trigger surface
- a lean `SKILL.md`
- optional references, scripts, and evals
- a generated visual HTML overview for each newly initialized skill
- neutral source metadata plus client-specific adapters
- governance, promotion, and portability checks built into the default flow
@@ -51,8 +52,8 @@ Read it in 10 seconds:
Or use the unified authoring CLI:
```bash
python3 scripts/yao.py validate .
python3 scripts/yao.py workspace-flow --target root --label first-pass
python3 scripts/yao.py init my-skill --description "Describe what the skill does."
python3 scripts/yao.py skill-report my-skill
python3 scripts/yao.py package . --platform generic --output-dir dist
```
+2 -1
View File
@@ -9,6 +9,7 @@ Il transforme des workflows bruts, des transcripts, des prompts, des notes et de
- une surface de déclenchement claire
- un `SKILL.md` léger
- des references, scripts et evals optionnels
- un rapport HTML minimaliste en fond blanc généré automatiquement pour chaque nouveau skill
- des métadonnées sources neutres et des adaptateurs spécifiques au client
- des contrôles de gouvernance, de promotion et de portabilité intégrés au flux standard
@@ -36,7 +37,7 @@ Lecture en 10 secondes :
1. Décrivez le workflow, l'ensemble de prompts ou la tâche répétée que vous voulez transformer en skill.
2. Utilisez `yao-meta-skill` pour générer ou améliorer le paquet en mode scaffold, production ou library.
3. Exécutez `context_sizer.py`, `trigger_eval.py` et `cross_packager.py` selon le besoin pour valider et exporter le résultat.
3. Chaque nouveau skill reçoit aussi `reports/skill-overview.html`, pour comprendre rapidement son architecture, sa logique, son usage et ses points forts.
## Results
+2 -1
View File
@@ -9,6 +9,7 @@
- 明確なトリガー面
- 軽量な `SKILL.md`
- 必要に応じた references、scripts、evals
- 新しい skill ごとに、白背景の簡潔な HTML overview を自動生成
- 中立的なソースメタデータとクライアント別アダプタ
- ガバナンス、昇格判定、portability チェックを標準フローに内蔵
@@ -36,7 +37,7 @@ flowchart LR
1. skill 化したい workflow、prompt 集合、または反復タスクを説明します。
2. `yao-meta-skill` を使って scaffold、production、library のいずれかのモードでパッケージを生成または改善します。
3. 必要に応じて `context_sizer.py``trigger_eval.py``cross_packager.py` を実行し、検証と出力を行います。
3. 新しく作成した skill には `reports/skill-overview.html` が付き、構造、ロジック、使い方、強みをすぐ確認できます。
## Results
+2 -1
View File
@@ -9,6 +9,7 @@
- понятной поверхностью срабатывания
- компактным `SKILL.md`
- необязательными references, scripts и evals
- автоматически создаваемым минималистичным HTML-обзором на белом фоне для каждого нового skill
- нейтральными исходными метаданными и клиентскими адаптерами
- встроенными проверками governance, promotion и portability в стандартном потоке
@@ -36,7 +37,7 @@ flowchart LR
1. Опишите workflow, набор prompts или повторяющуюся задачу, которую хотите превратить в skill.
2. Используйте `yao-meta-skill`, чтобы сгенерировать или улучшить пакет в режиме scaffold, production или library.
3. При необходимости запустите `context_sizer.py`, `trigger_eval.py` и `cross_packager.py`, чтобы проверить и экспортировать результат.
3. Каждый новый skill также получает `reports/skill-overview.html`, чтобы быстро понять архитектуру, логику, способ использования и сильные стороны.
## Results
+2 -1
View File
@@ -9,6 +9,7 @@
- 清晰的触发面
- 精简的 `SKILL.md`
- 可选的 references、scripts 和 evals
- 新建 skill 时自动生成一份极简白底 HTML 可视化说明
- 中性的源元数据以及面向不同客户端的适配层
- 内建的治理、晋升和 portability 检查
@@ -36,7 +37,7 @@ flowchart LR
1. 先描述你想沉淀成 skill 的 workflow、prompt 集合或重复任务。
2. 使用 `yao-meta-skill` 以 scaffold、production 或 library 模式生成或改进 skill 包。
3. 按需要运行 `context_sizer.py``trigger_eval.py``cross_packager.py` 来检查并导出结果
3. 新建 skill 后,会默认附带 `reports/skill-overview.html`,方便快速理解架构、逻辑、使用方法和优势
## Results
+85 -4
View File
@@ -1,7 +1,11 @@
#!/usr/bin/env python3
import argparse
import json
from datetime import date
from pathlib import Path
from render_skill_overview import render_skill_overview
SKILL_TEMPLATE = """---
name: {name}
@@ -18,10 +22,33 @@ description: {description}
"""
README_TEMPLATE = """# {title}
## What It Does
`{name}` is a reusable skill package for this job:
> {description}
## How To Use
1. Load the skill through `SKILL.md`.
2. Follow the workflow steps in `SKILL.md`.
3. Check `reports/skill-overview.html` if you want a fast visual explanation of the package.
## Package Map
- `SKILL.md`: trigger and workflow entrypoint
- `agents/interface.yaml`: portable interface metadata
- `manifest.json`: lifecycle and packaging metadata
- `reports/skill-overview.html`: visual overview report
"""
INTERFACE_TEMPLATE = """interface:
display_name: "{title}"
short_description: "{short_description}"
default_prompt: "Use ${name} to ..."
default_prompt: "Use ${name} when you need to {default_prompt}."
compatibility:
canonical_format: "agent-skills"
adapter_targets:
@@ -45,6 +72,31 @@ compatibility:
"""
def build_manifest(name: str) -> dict:
return {
"name": name,
"version": "0.1.0",
"owner": "Yao Team",
"updated_at": str(date.today()),
"status": "active",
"maturity_tier": "scaffold",
"lifecycle_stage": "scaffold",
"context_budget_tier": "scaffold",
"review_cadence": "ad-hoc",
"target_platforms": [
"openai",
"claude",
"generic",
"agent-skills-compatible",
],
"factory_components": [
"references",
"scripts",
"reports",
],
}
def main() -> None:
parser = argparse.ArgumentParser(description="Initialize a minimal skill package.")
parser.add_argument("name", help="skill folder and frontmatter name")
@@ -58,12 +110,41 @@ def main() -> None:
(root / "agents").mkdir(parents=True, exist_ok=True)
(root / "references").mkdir(exist_ok=True)
(root / "scripts").mkdir(exist_ok=True)
(root / "reports").mkdir(exist_ok=True)
(root / "SKILL.md").write_text(SKILL_TEMPLATE.format(name=args.name, description=args.description, title=title), encoding="utf-8")
(root / "agents" / "interface.yaml").write_text(
INTERFACE_TEMPLATE.format(name=args.name, title=title, short_description=args.description[:80]),
(root / "README.md").write_text(
README_TEMPLATE.format(name=args.name, description=args.description, title=title),
encoding="utf-8",
)
print(str(root))
(root / "agents" / "interface.yaml").write_text(
INTERFACE_TEMPLATE.format(
name=args.name,
title=title,
short_description=args.description[:80],
default_prompt=args.description.rstrip("."),
),
encoding="utf-8",
)
(root / "manifest.json").write_text(
json.dumps(build_manifest(args.name), ensure_ascii=False, indent=2),
encoding="utf-8",
)
overview = render_skill_overview(root)
print(
json.dumps(
{
"ok": True,
"root": str(root),
"artifacts": {
"readme": str(root / "README.md"),
"manifest": str(root / "manifest.json"),
**overview["artifacts"],
},
},
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
+484
View File
@@ -0,0 +1,484 @@
#!/usr/bin/env python3
import argparse
import html
import json
import re
from datetime import date
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
yaml = None
KNOWN_ENTRIES = [
("SKILL.md", "Skill entrypoint"),
("README.md", "Human-readable usage guide"),
("agents/interface.yaml", "Neutral interface metadata"),
("manifest.json", "Lifecycle and portability metadata"),
("references", "Extended guidance and reusable notes"),
("scripts", "Deterministic helpers or local tooling"),
("evals", "Trigger and quality checks"),
("reports", "Generated evidence and overview artifacts"),
]
def parse_frontmatter(text: str) -> tuple[dict, str]:
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return {}, text
try:
end_index = lines[1:].index("---") + 1
except ValueError:
return {}, text
frontmatter_text = "\n".join(lines[1:end_index])
body = "\n".join(lines[end_index + 1 :]).lstrip()
if yaml is not None:
data = yaml.safe_load(frontmatter_text) or {}
return data if isinstance(data, dict) else {}, body
data = {}
for line in frontmatter_text.splitlines():
if ":" not in line:
continue
key, value = line.split(":", 1)
data[key.strip()] = value.strip().strip('"')
return data, body
def load_yaml(path: Path) -> dict:
if not path.exists():
return {}
text = path.read_text(encoding="utf-8")
if yaml is not None:
payload = yaml.safe_load(text) or {}
return payload if isinstance(payload, dict) else {}
return {}
def load_json(path: Path) -> dict:
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def extract_title(body: str, fallback: str) -> str:
for line in body.splitlines():
if line.startswith("# "):
return line[2:].strip()
return fallback
def parse_sections(body: str) -> dict[str, str]:
sections: dict[str, list[str]] = {}
current = "_preamble"
sections[current] = []
for line in body.splitlines():
if line.startswith("## "):
current = line[3:].strip()
sections[current] = []
continue
sections[current].append(line)
return {name: "\n".join(lines).strip() for name, lines in sections.items()}
def extract_list_items(text: str) -> list[str]:
items = []
for line in text.splitlines():
stripped = line.strip()
if not stripped:
continue
ordered = re.match(r"^\d+\.\s+(.*)$", stripped)
bullet = re.match(r"^[-*]\s+(.*)$", stripped)
match = ordered or bullet
if match:
items.append(match.group(1).strip())
return items
def summarize_logic(sections: dict[str, str]) -> list[str]:
for key in ("Compact Workflow", "Workflow", "How It Works", "Logic", "Quick Start"):
if key in sections:
items = extract_list_items(sections[key])
if items:
return items[:5]
preamble_items = extract_list_items(sections.get("_preamble", ""))
return preamble_items[:5]
def summarize_usage(sections: dict[str, str], default_prompt: str, description: str) -> list[str]:
usage = []
for key in ("How To Use", "Quick Start", "Usage", "Runbook"):
if key in sections:
usage = extract_list_items(sections[key])
if usage:
return usage[:5]
if default_prompt:
usage.append(default_prompt)
usage.append(f"Use this skill when the request matches: {description}")
return usage[:4]
def package_entries(skill_dir: Path) -> list[dict]:
items = []
for rel_path, label in KNOWN_ENTRIES:
target = skill_dir / rel_path
if target.exists():
kind = "folder" if target.is_dir() else "file"
items.append({"path": rel_path, "label": label, "kind": kind})
return items
def derive_strengths(skill_dir: Path, metadata: dict) -> list[str]:
strengths = ["Lean trigger surface anchored in frontmatter description."]
if (skill_dir / "agents" / "interface.yaml").exists():
strengths.append("Portable interface metadata is already packaged for adapter-based export.")
if (skill_dir / "references").exists() and any((skill_dir / "references").iterdir()):
strengths.append("Long guidance is separated into references so the entrypoint can stay compact.")
if (skill_dir / "scripts").exists() and any((skill_dir / "scripts").iterdir()):
strengths.append("Deterministic helpers are packaged with the skill instead of hidden in prompt text.")
if (skill_dir / "evals").exists() and any((skill_dir / "evals").iterdir()):
strengths.append("The package includes quality gates or trigger checks that can travel with the skill.")
if metadata.get("maturity_tier"):
strengths.append(f"Lifecycle metadata is explicit, with maturity tier set to `{metadata['maturity_tier']}`.")
return strengths[:5]
def card_items(interface_data: dict, logic_steps: list[str], package_map: list[dict], usage_steps: list[str], description: str) -> list[dict]:
compatibility = interface_data.get("compatibility", {})
execution = compatibility.get("execution", {})
activation = compatibility.get("activation", {})
return [
{
"title": "Trigger",
"body": description,
"meta": [
f"Activation: {activation.get('mode', 'manual')}",
f"Context: {execution.get('context', 'inline')}",
],
},
{
"title": "Logic",
"body": logic_steps or ["Understand the request", "Execute the task", "Validate the result"],
"meta": [],
},
{
"title": "Usage",
"body": usage_steps,
"meta": [],
},
{
"title": "Package",
"body": [entry["path"] for entry in package_map[:6]],
"meta": [f"{len(package_map)} structured entries detected"],
},
]
def build_summary(skill_dir: Path) -> dict:
skill_text = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
frontmatter, body = parse_frontmatter(skill_text)
interface_data = load_yaml(skill_dir / "agents" / "interface.yaml")
manifest = load_json(skill_dir / "manifest.json")
sections = parse_sections(body)
name = frontmatter.get("name", skill_dir.name)
description = frontmatter.get("description", "No description found.")
title = extract_title(body, name.replace("-", " ").title())
display_name = interface_data.get("interface", {}).get("display_name", title)
default_prompt = interface_data.get("interface", {}).get("default_prompt", "")
logic_steps = summarize_logic(sections)
usage_steps = summarize_usage(sections, default_prompt, description)
package_map = package_entries(skill_dir)
strengths = derive_strengths(skill_dir, manifest)
return {
"name": name,
"title": title,
"display_name": display_name,
"description": description,
"logic_steps": logic_steps,
"usage_steps": usage_steps,
"package_map": package_map,
"strengths": strengths,
"cards": card_items(interface_data, logic_steps, package_map, usage_steps, description),
"metadata": {
"canonical_format": interface_data.get("compatibility", {}).get("canonical_format", "agent-skills"),
"targets": interface_data.get("compatibility", {}).get("adapter_targets", []),
"maturity_tier": manifest.get("maturity_tier", "scaffold"),
"updated_at": manifest.get("updated_at", str(date.today())),
},
}
def render_card_body(card: dict) -> str:
body = card["body"]
if isinstance(body, list):
items = "".join(f"<li>{html.escape(str(item))}</li>" for item in body)
body_html = f"<ol>{items}</ol>"
else:
body_html = f"<p>{html.escape(str(body))}</p>"
meta = "".join(f"<span>{html.escape(str(item))}</span>" for item in card.get("meta", []))
meta_html = f"<div class='meta'>{meta}</div>" if meta else ""
return f"<div class='card'><h3>{html.escape(card['title'])}</h3>{body_html}{meta_html}</div>"
def render_html(summary: dict) -> str:
target_badges = "".join(f"<span>{html.escape(str(target))}</span>" for target in summary["metadata"]["targets"])
cards_html = "".join(render_card_body(card) for card in summary["cards"])
strengths_html = "".join(f"<li>{html.escape(item)}</li>" for item in summary["strengths"])
package_rows = "".join(
f"<tr><td>{html.escape(item['path'])}</td><td>{html.escape(item['label'])}</td><td>{html.escape(item['kind'])}</td></tr>"
for item in summary["package_map"]
)
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(summary['display_name'])} Overview</title>
<style>
:root {{
--text: #101010;
--muted: #666666;
--line: #e8e6e1;
--soft: #f7f6f3;
--white: #ffffff;
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
background: var(--white);
color: var(--text);
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
}}
.wrap {{
max-width: 1120px;
margin: 0 auto;
padding: 40px 24px 80px;
}}
.eyebrow {{
margin: 0 0 12px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}}
h1, h2, h3 {{
margin: 0;
font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
font-weight: 600;
letter-spacing: -0.02em;
}}
h1 {{
font-size: clamp(2.4rem, 5vw, 4.4rem);
line-height: 1.02;
max-width: 10ch;
}}
.lead {{
max-width: 760px;
margin: 18px 0 0;
font-size: 1.05rem;
color: #222;
}}
.hero-meta, .badges {{
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 22px;
}}
.hero-meta span, .badges span {{
display: inline-flex;
align-items: center;
padding: 8px 12px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
background: var(--white);
font-size: 13px;
}}
section {{
padding-top: 28px;
margin-top: 28px;
border-top: 1px solid var(--line);
}}
.section-head {{
display: grid;
grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
gap: 24px;
align-items: start;
}}
.section-head p {{
margin: 8px 0 0;
color: var(--muted);
max-width: 40ch;
}}
.cards {{
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}}
.card {{
min-height: 220px;
padding: 22px;
border: 1px solid var(--line);
border-radius: 20px;
background: var(--white);
}}
.card h3 {{
font-size: 1.2rem;
margin-bottom: 14px;
}}
.card p, .card li {{
color: #222;
margin: 0;
}}
.card ol, .card ul {{
margin: 0;
padding-left: 20px;
display: grid;
gap: 10px;
}}
.meta {{
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}}
.meta span {{
padding: 6px 10px;
border-radius: 999px;
background: var(--soft);
color: var(--muted);
font-size: 12px;
}}
.strengths {{
margin: 0;
padding-left: 20px;
display: grid;
gap: 12px;
}}
table {{
width: 100%;
border-collapse: collapse;
font-size: 14px;
}}
th, td {{
padding: 14px 10px;
text-align: left;
border-bottom: 1px solid var(--line);
vertical-align: top;
}}
th {{
color: var(--muted);
font-weight: 600;
}}
@media (max-width: 900px) {{
.section-head {{ grid-template-columns: 1fr; }}
.cards {{ grid-template-columns: 1fr; }}
.card {{ min-height: auto; }}
}}
</style>
</head>
<body>
<div class="wrap">
<p class="eyebrow">Skill Overview</p>
<h1>{html.escape(summary["display_name"])}</h1>
<p class="lead">{html.escape(summary["description"])}</p>
<div class="hero-meta">
<span>Skill name: {html.escape(summary["name"])}</span>
<span>Maturity: {html.escape(summary["metadata"]["maturity_tier"])}</span>
<span>Format: {html.escape(summary["metadata"]["canonical_format"])}</span>
<span>Updated: {html.escape(summary["metadata"]["updated_at"])}</span>
</div>
<div class="badges">{target_badges}</div>
<section>
<div class="section-head">
<div>
<h2>Architecture</h2>
<p>One clear flow: define the boundary, choose the right structure, run the checks, then ship a reusable package.</p>
</div>
<div class="cards">{cards_html}</div>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>Why It Works</h2>
<p>These are the strengths the package already makes explicit instead of leaving hidden in prompt text.</p>
</div>
<ul class="strengths">{strengths_html}</ul>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>Package Map</h2>
<p>Use this map to understand what lives where before you read the whole package.</p>
</div>
<div>
<table>
<thead>
<tr><th>Path</th><th>Role</th><th>Type</th></tr>
</thead>
<tbody>{package_rows}</tbody>
</table>
</div>
</div>
</section>
</div>
</body>
</html>
"""
def render_skill_overview(skill_dir: Path, output_html: Path | None = None, output_json: Path | None = None) -> dict:
skill_dir = skill_dir.resolve()
reports_dir = skill_dir / "reports"
reports_dir.mkdir(parents=True, exist_ok=True)
output_html = output_html or reports_dir / "skill-overview.html"
output_json = output_json or reports_dir / "skill-overview.json"
summary = build_summary(skill_dir)
output_html.write_text(render_html(summary), encoding="utf-8")
output_json.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
return {
"ok": True,
"skill_dir": str(skill_dir),
"artifacts": {
"html": str(output_html),
"json": str(output_json),
},
"summary": summary,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Render a visual overview report for a skill package.")
parser.add_argument("skill_dir", nargs="?", default=".")
parser.add_argument("--output-html")
parser.add_argument("--output-json")
args = parser.parse_args()
result = render_skill_overview(
Path(args.skill_dir),
Path(args.output_html).resolve() if args.output_html else None,
Path(args.output_json).resolve() if args.output_json else None,
)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+19 -1
View File
@@ -111,7 +111,7 @@ def command_init(args: argparse.Namespace) -> int:
*(["--title", args.title] if args.title else []),
],
)
print(json.dumps(result, ensure_ascii=False, indent=2))
print(json.dumps(result["payload"] if result["payload"] is not None else result, ensure_ascii=False, indent=2))
return 0 if result["ok"] else 2
@@ -213,6 +213,18 @@ def command_report(args: argparse.Namespace) -> int:
return 0 if report["ok"] else 2
def command_skill_report(args: argparse.Namespace) -> int:
skill_dir = str(Path(args.skill_dir).resolve())
cmd = [skill_dir]
if args.output_html:
cmd.extend(["--output-html", args.output_html])
if args.output_json:
cmd.extend(["--output-json", args.output_json])
result = run_script("render_skill_overview.py", cmd)
print(json.dumps(result["payload"] if result["payload"] is not None else result, ensure_ascii=False, indent=2))
return 0 if result["ok"] else 2
def command_review(args: argparse.Namespace) -> int:
target_name = resolve_promotion_target(args.target)
bundle_dir = ROOT / "reports" / "iteration_bundles" / target_name
@@ -416,6 +428,12 @@ def build_parser() -> argparse.ArgumentParser:
report_cmd.add_argument("--refresh-optimization", action="store_true")
report_cmd.set_defaults(func=command_report)
skill_report_cmd = subparsers.add_parser("skill-report", help="Render a visual overview report for a skill package.")
skill_report_cmd.add_argument("skill_dir", nargs="?", default=".")
skill_report_cmd.add_argument("--output-html")
skill_report_cmd.add_argument("--output-json")
skill_report_cmd.set_defaults(func=command_skill_report)
package_cmd = subparsers.add_parser("package", help="Export compatibility artifacts for selected targets.")
package_cmd.add_argument("skill_dir", nargs="?", default=".")
package_cmd.add_argument("--platform", action="append")
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CLI = ROOT / "scripts" / "yao.py"
def run(*args: str) -> dict:
proc = subprocess.run(
[sys.executable, str(CLI), *args],
cwd=ROOT,
capture_output=True,
text=True,
)
payload = json.loads(proc.stdout)
return {
"ok": proc.returncode == 0,
"returncode": proc.returncode,
"payload": payload,
"stderr": proc.stderr,
}
def main() -> None:
tmp_root = ROOT / "tests" / "tmp_skill_overview"
if tmp_root.exists():
subprocess.run(["rm", "-rf", str(tmp_root)], check=True)
tmp_root.mkdir(parents=True, exist_ok=True)
init_result = run(
"init",
"skill-overview-demo",
"--description",
"Turn rough requests into a compact reusable demo skill.",
"--output-dir",
str(tmp_root),
)
assert init_result["ok"], init_result
created = tmp_root / "skill-overview-demo"
assert (created / "README.md").exists(), created
assert (created / "manifest.json").exists(), created
assert (created / "reports" / "skill-overview.html").exists(), created
assert (created / "reports" / "skill-overview.json").exists(), created
rerender_result = run("skill-report", str(created))
assert rerender_result["ok"], rerender_result
assert rerender_result["payload"]["artifacts"]["html"].endswith("reports/skill-overview.html"), rerender_result
report_html = (created / "reports" / "skill-overview.html").read_text(encoding="utf-8")
assert "Skill Overview" in report_html, report_html[:200]
assert "Architecture" in report_html, report_html[:400]
assert "Why It Works" in report_html, report_html[:600]
print(json.dumps({"ok": True}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+7 -3
View File
@@ -33,15 +33,19 @@ def main() -> None:
init_result = run("init", "cli-demo-skill", "--description", "CLI demo skill.", "--output-dir", str(tmp_root))
assert init_result["ok"], init_result
created = Path(init_result["payload"]["stdout"].strip()) if "stdout" in init_result["payload"] else tmp_root / "cli-demo-skill"
if not created.exists():
created = tmp_root / "cli-demo-skill"
created = Path(init_result["payload"]["root"])
assert (created / "SKILL.md").exists(), created
assert (created / "README.md").exists(), created
assert (created / "reports" / "skill-overview.html").exists(), created
validate_result = run("validate", str(created))
assert validate_result["ok"], validate_result
assert len(validate_result["payload"]["steps"]) == 4, validate_result
skill_report_result = run("skill-report", str(created))
assert skill_report_result["ok"], skill_report_result
assert skill_report_result["payload"]["artifacts"]["html"].endswith("reports/skill-overview.html"), skill_report_result
optimize_result = run("optimize-description", "--target", "root")
assert optimize_result["ok"], optimize_result
assert optimize_result["payload"]["winner"]["label"] == "Current", optimize_result