diff --git a/reports/context_budget.json b/reports/context_budget.json index e408ca1c..8519419f 100644 --- a/reports/context_budget.json +++ b/reports/context_budget.json @@ -6,9 +6,9 @@ "context_budget_tier": "production", "context_budget_limit": 1000, "skill_body_tokens": 824, - "other_text_tokens": 271901, + "other_text_tokens": 274311, "estimated_initial_load_tokens": 1000, - "estimated_total_text_tokens": 272725, + "estimated_total_text_tokens": 275135, "relevant_file_count": 165, "unused_resource_dirs": [], "quality_signal_points": 140, diff --git a/scripts/render_intent_dialogue.py b/scripts/render_intent_dialogue.py index 5af75295..dc165673 100644 --- a/scripts/render_intent_dialogue.py +++ b/scripts/render_intent_dialogue.py @@ -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" diff --git a/scripts/render_iteration_directions.py b/scripts/render_iteration_directions.py index af0eb9ed..921b9107 100644 --- a/scripts/render_iteration_directions.py +++ b/scripts/render_iteration_directions.py @@ -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" diff --git a/scripts/render_review_viewer.py b/scripts/render_review_viewer.py index 0424c49c..3e81c4c4 100644 --- a/scripts/render_review_viewer.py +++ b/scripts/render_review_viewer.py @@ -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"
  • {html.escape(item)}
  • " for item in overview.get("strengths", [])) logic_items = "".join(f"
  • {html.escape(item)}
  • " for item in overview.get("logic_steps", [])) @@ -225,6 +247,23 @@ def render_html(report: dict) -> str: else: promotion_html = "

    No promotion summary is attached to this package yet.

    " + benchmark_html = "" + if benchmark_rows: + benchmark_html = "".join( + ( + "
    " + f"

    {html.escape(item['name'])}

    " + "

    Borrow now

    " + + ("" if item.get("borrow") else "

    No borrow cues recorded.

    ") + + "

    Avoid

    " + + ("" if item.get("avoid") else "

    No avoid cues recorded.

    ") + + "
    " + ) + for item in benchmark_rows + ) + else: + benchmark_html = "

    No GitHub benchmark scan has been attached to this package yet.

    " + return f""" @@ -414,6 +453,21 @@ def render_html(report: dict) -> str: +
    +
    +

    Reference coach

    +
    {benchmark_html}
    +
    +
    +

    Decide before you deepen

    + +
    +
    +

    Top three next moves

    {direction_cards}
    diff --git a/scripts/render_skill_overview.py b/scripts/render_skill_overview.py index 5f27c25a..a67849e0 100644 --- a/scripts/render_skill_overview.py +++ b/scripts/render_skill_overview.py @@ -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"{html.escape(str(target))}" for target in summary["metadata"]["targets"]) cards_html = "".join(render_card_body(card) for card in summary["cards"]) strengths_html = "".join(f"
  • {html.escape(item)}
  • " for item in summary["strengths"]) + introduction_html = "".join(f"
  • {html.escape(item)}
  • " for item in summary.get("introduction", [])) package_rows = "".join( f"{html.escape(item['path'])}{html.escape(item['label'])}{html.escape(item['kind'])}" for item in summary["package_map"] ) + benchmark_cards = "".join( + ( + "
    " + f"

    {html.escape(item['name'])}

    " + "

    Borrow now

    " + + ("" if item.get("borrow") else "

    None yet.

    ") + + "

    Avoid

    " + + ("" if item.get("avoid") else "

    None yet.

    ") + + "
    " + ) + for item in summary.get("benchmark_highlights", []) + ) return f""" @@ -411,6 +452,16 @@ def render_html(summary: dict) -> str:
    +
    +
    +
    +

    How to introduce this skill

    +

    Use this section when a first-time reader needs the shape and intent quickly, before reading the full package.

    +
    + +
    +
    +
    @@ -437,6 +488,16 @@ def render_html(summary: dict) -> str:
    + +
    +
    +
    +

    Patterns worth borrowing now

    +

    If a GitHub benchmark scan exists, surface the strongest borrow and avoid cues here instead of hiding them in raw report files.

    +
    +
    {benchmark_cards or "

    No benchmark scan recorded yet. Run the GitHub benchmark scan first.

    "}
    +
    +
    diff --git a/scripts/yao.py b/scripts/yao.py index 009919b2..7dcebcaa 100644 --- a/scripts/yao.py +++ b/scripts/yao.py @@ -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)) diff --git a/tests/verify_iteration_directions.py b/tests/verify_iteration_directions.py index 2c4191b4..9645e287 100644 --- a/tests/verify_iteration_directions.py +++ b/tests/verify_iteration_directions.py @@ -37,6 +37,8 @@ def main() -> None: created = Path(init_payload["root"]) report = run("iteration-directions", str(created)) assert report["summary"]["selection_rule"], report + assert report["summary"]["recommended_now"], report + assert report["summary"]["defer_for_now"], report assert len(report["directions"]) == 3, report titles = {item["title"] for item in report["directions"]} assert "Tighten trigger and exclusions" in titles or "Add the first execution asset" in titles, titles diff --git a/tests/verify_review_viewer.py b/tests/verify_review_viewer.py index 08f21345..16978562 100644 --- a/tests/verify_review_viewer.py +++ b/tests/verify_review_viewer.py @@ -45,6 +45,7 @@ def main() -> None: html_text = html_path.read_text(encoding="utf-8") assert "Architecture at a glance" in html_text, html_text[:500] assert "Compare view" in html_text, html_text[:500] + assert "Reference coach" in html_text, html_text[:900] assert "Top three next moves" in html_text, html_text[:500] print(json.dumps({"ok": True}, ensure_ascii=False, indent=2)) diff --git a/tests/verify_skill_overview.py b/tests/verify_skill_overview.py index 78ca6e36..e0b3801c 100644 --- a/tests/verify_skill_overview.py +++ b/tests/verify_skill_overview.py @@ -59,6 +59,8 @@ def main() -> None: 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] + assert "How to introduce this skill" in report_html, report_html[:900] + assert "Patterns worth borrowing now" in report_html, report_html[:1200] intent_text = (created / "reports" / "intent-dialogue.md").read_text(encoding="utf-8") assert "Questions To Ask" in intent_text, intent_text[:400]