feat: deepen discovery and review experience

This commit is contained in:
yaojingang
2026-04-14 11:21:16 +08:00
parent 5b24cb2cae
commit 881a8317c6
9 changed files with 248 additions and 21 deletions
+74 -13
View File
@@ -100,6 +100,26 @@ def build_questions(focus: str) -> list[dict]:
return base
def build_opening_styles() -> list[dict]:
return [
{
"label": "温柔陪伴型",
"best_when": "用户想法还散、还在试探,或者需要先被接住。",
"message": "我们先不急着把它说成一个很完整的 skill。你就像跟我聊天一样,先说说你最想让它以后稳稳接住哪类重复工作;如果它做得很理想,最后应该交回你一个什么结果。",
},
{
"label": "专业教练型",
"best_when": "用户目标比较明确,希望被高效带着走。",
"message": "我们先把这件事讲清楚,再决定 skill 怎么设计。你先告诉我三件事:它要接住的重复任务是什么,别人通常会给它什么材料,最后你希望它交付什么结果。",
},
{
"label": "共创伙伴型",
"best_when": "用户已经有一些想法,希望一起打磨,不想被填表。",
"message": "我们把这次当成一次共创。你先给我一个粗糙版本就行,我先帮你看它真正的核心任务是什么,再一起决定边界、结构和接下来最值的一步。",
},
]
def build_summary(skill_dir: Path) -> dict:
skill_text = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
frontmatter, body = parse_frontmatter(skill_text)
@@ -108,6 +128,7 @@ def build_summary(skill_dir: Path) -> dict:
title = extract_title(body, name.replace("-", " ").title())
focus = classify_focus(description)
questions = build_questions(focus)
opening_styles = build_opening_styles()
output = {
"capability_sentence": f"{title} should turn a recurring request into a reliable reusable output without widening the boundary unnecessarily.",
"required_capture": [
@@ -126,8 +147,20 @@ def build_summary(skill_dir: Path) -> dict:
"title": title,
"description": description,
"focus": focus,
"opening_frame": "Let's shape the skill around the real work, the desired outcome, and the standards you care about before we start adding structure.",
"opening_frame": "Let's start from the real work, the result you care about, and the standards that matter here. We can make the structure clearer after that.",
"reference_note": "If you already have examples you admire, bring them in. We will learn the pattern, not copy the source.",
"conversation_path": [
"Start with the user's own words, not package vocabulary.",
"Reflect the job, output, and non-goals back in one clean sentence.",
"Only then offer a tiny scaffold if it would help the user move faster.",
],
"opening_styles": opening_styles,
"optional_scaffold": [
"The repeated job it should take over",
"The real inputs people will hand to it",
"The useful output it should hand back",
"What it should clearly refuse",
],
"questions": questions,
"output": output,
}
@@ -143,20 +176,45 @@ def render_markdown(summary: dict) -> str:
"",
summary["opening_frame"],
"",
"## Why Start Here",
"",
"Use this short dialogue before deep authoring. The goal is to learn the real job, output, exclusions, and constraints so the first package is small but accurate.",
"",
"## Current Anchor",
"",
f"- Title: `{summary['title']}`",
f"- Description: {summary['description']}",
f"- Focus: `{summary['focus']}`",
f"- Reference note: {summary['reference_note']}",
"",
"## Questions To Ask",
"## Opening Tone Options",
"",
]
for item in summary["opening_styles"]:
lines.extend(
[
f"### {item['label']}",
"",
f"- Best when: {item['best_when']}",
f"- Example: {item['message']}",
"",
]
)
lines.extend(
[
"## Conversation Path",
"",
]
)
for idx, item in enumerate(summary["conversation_path"], start=1):
lines.append(f"{idx}. {item}")
lines.extend(
[
"",
"## Why Start Here",
"",
"Use this short dialogue before deep authoring. The goal is to learn the real job, output, exclusions, and constraints so the first package is small but accurate.",
"",
"## Current Anchor",
"",
f"- Title: `{summary['title']}`",
f"- Description: {summary['description']}",
f"- Focus: `{summary['focus']}`",
f"- Reference note: {summary['reference_note']}",
"",
"## Questions To Ask",
"",
]
)
for idx, item in enumerate(summary["questions"], start=1):
lines.extend(
[
@@ -171,8 +229,11 @@ def render_markdown(summary: dict) -> str:
"",
f"- Capability sentence: {summary['output']['capability_sentence']}",
f"- Recommended first gate: `{summary['output']['recommended_first_gate']}`",
"- Tiny optional scaffold:",
]
)
for item in summary["optional_scaffold"]:
lines.append(f" - {item}")
for item in summary["output"]["required_capture"]:
lines.append(f"- Capture: `{item}`")
return "\n".join(lines).strip() + "\n"
+45 -2
View File
@@ -37,6 +37,12 @@ def load_manifest(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def load_json(path: Path) -> dict:
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def extract_sections(body: str) -> dict[str, str]:
sections: dict[str, list[str]] = {}
current = "_preamble"
@@ -69,6 +75,8 @@ def direction(priority: int, title: str, why: str, actions: list[str], unlocks:
"why": why,
"actions": actions,
"unlocks": unlocks,
"do_now": "",
"wait_on": "",
}
@@ -77,6 +85,7 @@ def build_directions(skill_dir: Path) -> tuple[dict, list[dict]]:
frontmatter, body = parse_frontmatter(skill_text)
sections = extract_sections(body)
manifest = load_manifest(skill_dir / "manifest.json")
benchmark = load_json(skill_dir / "reports" / "github-benchmark-scan.json")
description = frontmatter.get("description", "")
maturity = manifest.get("maturity_tier", "scaffold")
@@ -109,6 +118,21 @@ def build_directions(skill_dir: Path) -> tuple[dict, list[dict]]:
"Stronger execution quality without bloating the entrypoint.",
)
)
if benchmark.get("repositories"):
top_repo = benchmark["repositories"][0]
candidates.append(
direction(
2,
"Borrow one proven pattern on purpose",
"You already have public benchmark objects. The next gain is to choose one pattern intentionally instead of absorbing everything loosely.",
[
f"Read the strongest pattern from {top_repo.get('full_name', 'the top benchmark repo')}.",
"Decide whether to borrow method, structure, execution, or portability, but only one of them first.",
"Record what you will not borrow so the package stays light.",
],
"A cleaner package shape with less accidental over-design.",
)
)
if maturity == "scaffold":
candidates.append(
direction(
@@ -169,11 +193,25 @@ def build_directions(skill_dir: Path) -> tuple[dict, list[dict]]:
)
chosen = sorted(candidates, key=lambda item: item["priority"])[:3]
for index, item in enumerate(chosen, start=1):
item["do_now"] = (
"Do this first."
if index == 1
else "Do this after the first move lands cleanly."
)
item["wait_on"] = (
"Wait to add broader structure until this move clearly improves reliability."
if index == 1
else "Wait until the package has evidence that this extra structure is justified."
)
summary = {
"skill_name": frontmatter.get("name", skill_dir.name),
"description": description,
"maturity_tier": maturity,
"selection_rule": "Pick the three smallest next steps that increase reliability more than they increase context cost.",
"recommended_now": chosen[0]["title"] if chosen else "",
"recommended_now_why": chosen[0]["why"] if chosen else "",
"defer_for_now": chosen[-1]["title"] if chosen else "",
}
return summary, chosen
@@ -186,6 +224,9 @@ def render_markdown(summary: dict, directions: list[dict]) -> str:
"",
f"- Maturity tier: `{summary['maturity_tier']}`",
f"- Selection rule: {summary['selection_rule']}",
f"- Start here: `{summary['recommended_now']}`",
f"- Why first: {summary['recommended_now_why']}",
f"- Defer for now: `{summary['defer_for_now']}`",
"",
"## Top 3 Next Moves",
"",
@@ -196,13 +237,15 @@ def render_markdown(summary: dict, directions: list[dict]) -> str:
f"### {item['priority']}. {item['title']}",
"",
f"- Why now: {item['why']}",
f"- Timing: {item['do_now']}",
"- Recommended actions:",
]
)
for action in item["actions"]:
lines.append(f" - {action}")
lines.append(f"- Unlocks: {item['unlocks']}")
lines.append("")
lines.append(f"- Unlocks: {item['unlocks']}")
lines.append(f"- Wait on: {item['wait_on']}")
lines.append("")
return "\n".join(lines).strip() + "\n"
+54
View File
@@ -43,6 +43,11 @@ def load_specific_promotion(skill_dir: Path) -> dict:
return payload if isinstance(payload, dict) else {}
def load_benchmark_summary(skill_dir: Path) -> dict:
payload = load_json(skill_dir / "reports" / "github-benchmark-scan.json")
return payload if isinstance(payload, dict) else {}
def ensure_report_inputs(skill_dir: Path) -> dict:
overview_json = skill_dir / "reports" / "skill-overview.json"
intent_json = skill_dir / "reports" / "intent-dialogue.json"
@@ -62,6 +67,7 @@ def ensure_report_inputs(skill_dir: Path) -> dict:
baseline = load_baseline_summary(skill_dir)
compare = load_specific_compare(skill_dir)
promotion = load_specific_promotion(skill_dir)
benchmark = load_benchmark_summary(skill_dir)
return {
"overview": overview,
"intent": intent,
@@ -71,6 +77,7 @@ def ensure_report_inputs(skill_dir: Path) -> dict:
"baseline": baseline,
"compare": compare,
"promotion": promotion,
"benchmark": benchmark,
}
@@ -112,6 +119,19 @@ def compare_rows(compare: dict) -> list[dict]:
return rows
def benchmark_cards(benchmark: dict) -> list[dict]:
cards = []
for repo in benchmark.get("repositories", [])[:3]:
cards.append(
{
"name": repo.get("full_name", "Unknown repo"),
"borrow": repo.get("borrow", [])[:2],
"avoid": repo.get("avoid", [])[:1],
}
)
return cards
def render_html(report: dict) -> str:
overview = report["overview"]
intent = report["intent"]
@@ -122,8 +142,10 @@ def render_html(report: dict) -> str:
baseline = report.get("baseline", {})
compare = report.get("compare", {})
promotion = report.get("promotion", {})
benchmark = report.get("benchmark", {})
architecture = architecture_steps(overview)
compare_table_rows = compare_rows(compare)
benchmark_rows = benchmark_cards(benchmark)
strength_items = "".join(f"<li>{html.escape(item)}</li>" for item in overview.get("strengths", []))
logic_items = "".join(f"<li>{html.escape(item)}</li>" for item in overview.get("logic_steps", []))
@@ -225,6 +247,23 @@ def render_html(report: dict) -> str:
else:
promotion_html = "<p class='minor'>No promotion summary is attached to this package yet.</p>"
benchmark_html = ""
if benchmark_rows:
benchmark_html = "".join(
(
"<div class='direction-card'>"
f"<h3>{html.escape(item['name'])}</h3>"
"<p><strong>Borrow now</strong></p>"
+ ("<ul>" + "".join(f"<li>{html.escape(borrow)}</li>" for borrow in item.get('borrow', [])) + "</ul>" if item.get("borrow") else "<p>No borrow cues recorded.</p>")
+ "<p><strong>Avoid</strong></p>"
+ ("<ul>" + "".join(f"<li>{html.escape(avoid)}</li>" for avoid in item.get('avoid', [])) + "</ul>" if item.get("avoid") else "<p>No avoid cues recorded.</p>")
+ "</div>"
)
for item in benchmark_rows
)
else:
benchmark_html = "<p class='minor'>No GitHub benchmark scan has been attached to this package yet.</p>"
return f"""<!DOCTYPE html>
<html lang="en">
<head>
@@ -414,6 +453,21 @@ def render_html(report: dict) -> str:
</div>
</section>
<section class="grid">
<div class="panel">
<h2>Reference coach</h2>
<div class="direction-grid">{benchmark_html}</div>
</div>
<div class="panel">
<h2>Decide before you deepen</h2>
<ul>
<li>Choose one pattern to borrow on purpose, not three at once.</li>
<li>State one thing this skill will not inherit from the benchmark objects.</li>
<li>Only deepen the package after that choice is visible in the boundary or execution flow.</li>
</ul>
</div>
</section>
<section>
<h2>Top three next moves</h2>
<div class="direction-grid">{direction_cards}</div>
+61
View File
@@ -66,6 +66,10 @@ def load_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def load_benchmark(skill_dir: Path) -> dict:
return load_json(skill_dir / "reports" / "github-benchmark-scan.json")
def extract_title(body: str, fallback: str) -> str:
for line in body.splitlines():
if line.startswith("# "):
@@ -179,6 +183,27 @@ def card_items(interface_data: dict, logic_steps: list[str], package_map: list[d
]
def introduction_lines(description: str) -> list[str]:
return [
f"This skill is trying to quietly take over: {description}",
"Start by clarifying the recurring job, the real input shape, and the output that lets the next person keep moving.",
"If the idea is still fuzzy, use the intent dialogue first and let the structure come second.",
]
def benchmark_highlights(benchmark: dict) -> list[dict]:
highlights = []
for repo in benchmark.get("repositories", [])[:3]:
highlights.append(
{
"name": repo.get("full_name", "Unknown repo"),
"borrow": repo.get("borrow", [])[:2],
"avoid": repo.get("avoid", [])[:1],
}
)
return highlights
def build_summary(skill_dir: Path) -> dict:
skill_text = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
frontmatter, body = parse_frontmatter(skill_text)
@@ -195,6 +220,7 @@ def build_summary(skill_dir: Path) -> dict:
usage_steps = summarize_usage(sections, default_prompt, description)
package_map = package_entries(skill_dir)
strengths = derive_strengths(skill_dir, manifest)
benchmark = load_benchmark(skill_dir)
return {
"name": name,
@@ -206,6 +232,8 @@ def build_summary(skill_dir: Path) -> dict:
"package_map": package_map,
"strengths": strengths,
"cards": card_items(interface_data, logic_steps, package_map, usage_steps, description),
"introduction": introduction_lines(description),
"benchmark_highlights": benchmark_highlights(benchmark),
"metadata": {
"canonical_format": interface_data.get("compatibility", {}).get("canonical_format", "agent-skills"),
"targets": interface_data.get("compatibility", {}).get("adapter_targets", []),
@@ -232,10 +260,23 @@ 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"])
introduction_html = "".join(f"<li>{html.escape(item)}</li>" for item in summary.get("introduction", []))
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"]
)
benchmark_cards = "".join(
(
"<div class='card'>"
f"<h3>{html.escape(item['name'])}</h3>"
"<p><strong>Borrow now</strong></p>"
+ ("<ul>" + "".join(f"<li>{html.escape(borrow)}</li>" for borrow in item.get("borrow", [])) + "</ul>" if item.get("borrow") else "<p>None yet.</p>")
+ "<p><strong>Avoid</strong></p>"
+ ("<ul>" + "".join(f"<li>{html.escape(avoid)}</li>" for avoid in item.get("avoid", [])) + "</ul>" if item.get("avoid") else "<p>None yet.</p>")
+ "</div>"
)
for item in summary.get("benchmark_highlights", [])
)
return f"""<!DOCTYPE html>
<html lang="en">
@@ -411,6 +452,16 @@ def render_html(summary: dict) -> str:
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>How to introduce this skill</h2>
<p>Use this section when a first-time reader needs the shape and intent quickly, before reading the full package.</p>
</div>
<ul class="strengths">{introduction_html}</ul>
</div>
</section>
<section>
<div class="section-head">
<div>
@@ -437,6 +488,16 @@ def render_html(summary: dict) -> str:
</div>
</div>
</section>
<section>
<div class="section-head">
<div>
<h2>Patterns worth borrowing now</h2>
<p>If a GitHub benchmark scan exists, surface the strongest borrow and avoid cues here instead of hiding them in raw report files.</p>
</div>
<div class="cards">{benchmark_cards or "<div class='card'><p>No benchmark scan recorded yet. Run the GitHub benchmark scan first.</p></div>"}</div>
</div>
</section>
</div>
</body>
</html>
+7 -4
View File
@@ -203,11 +203,12 @@ def command_init(args: argparse.Namespace) -> int:
def command_quickstart(args: argparse.Namespace) -> int:
sys.stderr.write("Let's shape this skill around the real work, the outcome, and the standards you care about before we add structure.\n")
sys.stderr.write("I will also scan a few strong public GitHub references first, so we can borrow the right patterns without copying the source.\n")
sys.stderr.write("Let's start gently and shape this skill around the real work, the outcome, and the standards you actually care about.\n")
sys.stderr.write("You do not need a perfect brief. A rough description is enough, and I will help tighten it.\n")
sys.stderr.write("I will also look at a few strong public GitHub references first, so we can borrow proven patterns without copying the source.\n")
name = args.name or prompt_with_default("Skill name", "my-skill")
job = args.job or prompt_with_default("What repeated work should this skill quietly take off your hands", "Turn a repeated workflow into a reusable skill.")
primary_output = args.primary_output or prompt_with_default("What should it reliably hand back", "A reusable skill package.")
job = args.job or prompt_with_default("What repeated work do you most want this skill to quietly take off your hands", "Turn a repeated workflow into a reusable skill.")
primary_output = args.primary_output or prompt_with_default("If it works well, what should it hand back so you can keep moving", "A reusable skill package.")
description = args.description or f"{job.rstrip('.')} Primary output: {primary_output.rstrip('.')}."
inferred_archetype, archetype_reason = infer_archetype(job, description)
archetype = args.archetype or prompt_with_default("Archetype (scaffold/production/library/governed)", inferred_archetype)
@@ -226,6 +227,7 @@ def command_quickstart(args: argparse.Namespace) -> int:
)
github_query = args.github_query or build_query(" ".join(filter(None, [job, primary_output, description])))
sys.stderr.write(f"GitHub benchmark query: {github_query}\n")
sys.stderr.write("I will use that query to pull three strong public examples, then surface only the patterns worth borrowing or avoiding.\n")
title = args.title or name.replace("-", " ").title()
guidance = archetype_guidance(archetype)
cmd = [
@@ -295,6 +297,7 @@ def command_quickstart(args: argparse.Namespace) -> int:
"Use reports/iteration-directions.md to choose only one high-value next move before adding more files.",
],
"borrow_prompt": benchmark_scan.get("borrow_prompt"),
"experience_note": "The first pass should feel more like guided co-creation than filling out a worksheet. Use the reports to explain, choose, and only then deepen the package.",
},
}
print(json.dumps(report, ensure_ascii=False, indent=2))