From b73fff025399bf81b7bcd62a4980ef6b998f6c2c Mon Sep 17 00:00:00 2001 From: yaojingang Date: Tue, 14 Apr 2026 14:39:52 +0800 Subject: [PATCH] feat: add conversational quickstart and diff studio --- README.md | 4 +- docs/README.fr-FR.md | 2 +- docs/README.ja-JP.md | 2 +- docs/README.ru-RU.md | 2 +- docs/README.zh-CN.md | 2 +- reports/context_budget.json | 4 +- scripts/render_review_viewer.py | 150 +++++++++++++++++++++++++++++++- scripts/yao.py | 62 ++++++++++--- tests/verify_review_viewer.py | 1 + tests/verify_yao_cli.py | 3 +- 10 files changed, 210 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index cc4de1bb..3a2275bb 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ It turns rough workflows, transcripts, prompts, notes, and runbooks into reusabl - a front-loaded intent dialogue that starts from the user's real work, desired outcome, and quality bar - an automatic GitHub benchmark scan that pulls the top three public repositories first, extracts borrow or avoid patterns, and then asks whether the user has references worth learning from - a generated visual HTML overview for each newly initialized skill -- a compact HTML review viewer for first-pass human review +- a side-by-side HTML review studio for first-pass human review - three high-value next iteration directions after the first package is created - a lightweight feedback log that does not require a full promotion cycle - a baseline compare report for with-skill vs baseline review -- an archetype-aware quickstart that steers new packages toward scaffold, production, library, or governed fits +- a conversation-style, archetype-aware quickstart that steers new packages toward scaffold, production, library, or governed fits - neutral source metadata plus client-specific adapters - governance, promotion, and portability checks built into the default flow diff --git a/docs/README.fr-FR.md b/docs/README.fr-FR.md index 486a2b90..851c4937 100644 --- a/docs/README.fr-FR.md +++ b/docs/README.fr-FR.md @@ -17,7 +17,7 @@ Il transforme des workflows bruts, des transcripts, des prompts, des notes et de - un review viewer HTML compact pour accélérer la première revue humaine - un feedback log léger pour éviter de lancer tout le flux de promotion à chaque tour - un rapport with-skill vs baseline pour visualiser rapidement le gain incrémental -- un quickstart sensible aux archetypes pour orienter un nouveau skill vers scaffold, production, library ou governed +- un quickstart conversationnel, sensible aux archetypes, pour orienter un nouveau skill vers scaffold, production, library ou governed - 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 diff --git a/docs/README.ja-JP.md b/docs/README.ja-JP.md index a538a8a4..002242e8 100644 --- a/docs/README.ja-JP.md +++ b/docs/README.ja-JP.md @@ -17,7 +17,7 @@ - 初回の人間レビューを助ける、コンパクトな HTML review viewer - 毎回フル promotion flow を通さずに使える軽量 feedback log - 増分価値をすばやく確認できる with-skill vs baseline 比較レポート -- scaffold / production / library / governed を案内する archetype-aware quickstart +- 会話型 discovery flow として scaffold / production / library / governed を案内する archetype-aware quickstart - 中立的なソースメタデータとクライアント別アダプタ - ガバナンス、昇格判定、portability チェックを標準フローに内蔵 diff --git a/docs/README.ru-RU.md b/docs/README.ru-RU.md index e9fab5ad..98360468 100644 --- a/docs/README.ru-RU.md +++ b/docs/README.ru-RU.md @@ -17,7 +17,7 @@ - компактным HTML review viewer для быстрой первой ручной оценки - легким feedback log, чтобы не запускать полный promotion flow на каждом цикле - отчетом with-skill vs baseline для быстрого сравнения инкрементальной пользы -- archetype-aware quickstart, который помогает выбрать scaffold, production, library или governed форму +- conversation-style archetype-aware quickstart, который ведет пользователя как discovery flow и помогает выбрать scaffold, production, library или governed форму - нейтральными исходными метаданными и клиентскими адаптерами - встроенными проверками governance, promotion и portability в стандартном потоке diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 6248edf1..60984110 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -17,7 +17,7 @@ - 提供一个紧凑的 HTML review viewer,方便第一次人工理解和评审 - 提供一个轻量 feedback log,不必每次都走完整 promotion 流程 - 提供一个 with-skill vs baseline 的对比报告,便于快速看增量收益 -- 提供一个 archetype-aware 的 quickstart,引导新 skill 落到 scaffold、production、library 或 governed 的合适形态 +- 提供一个更像对话发现流程的 archetype-aware quickstart,引导新 skill 落到 scaffold、production、library 或 governed 的合适形态 - 中性的源元数据以及面向不同客户端的适配层 - 内建的治理、晋升和 portability 检查 diff --git a/reports/context_budget.json b/reports/context_budget.json index 8519419f..f0537f09 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": 274311, + "other_text_tokens": 276100, "estimated_initial_load_tokens": 1000, - "estimated_total_text_tokens": 275135, + "estimated_total_text_tokens": 276924, "relevant_file_count": 165, "unused_resource_dirs": [], "quality_signal_points": 140, diff --git a/scripts/render_review_viewer.py b/scripts/render_review_viewer.py index 3e81c4c4..6ee72a72 100644 --- a/scripts/render_review_viewer.py +++ b/scripts/render_review_viewer.py @@ -2,6 +2,7 @@ import argparse import html import json +import re from pathlib import Path from render_intent_dialogue import render_intent_dialogue @@ -132,6 +133,67 @@ def benchmark_cards(benchmark: dict) -> list[dict]: return cards +def split_sentences(text: str) -> list[str]: + if not text: + return [] + parts = [item.strip() for item in re.split(r"(?<=[.!?])\s+", " ".join(text.split())) if item.strip()] + return parts + + +def metric_delta(current: int | float, baseline: int | float) -> str: + delta = current - baseline + if delta == 0: + return "0" + return f"{delta:+}" + + +def variant_diff_cards(compare: dict) -> list[dict]: + baseline = compare.get("baseline", {}) + current = compare.get("current_candidate", {}) + winner = compare.get("winner", {}) + variants = [ + ("Baseline", baseline), + ("Current", current), + (f"Winner — {winner.get('label', 'Winner')}", winner), + ] + baseline_sentences = split_sentences(baseline.get("description", "")) + baseline_set = set(baseline_sentences) + baseline_dev = baseline.get("dev", {}).get("total_errors", 0) + baseline_holdout = baseline.get("holdout", {}).get("total_errors", 0) + cards = [] + seen = set() + for label, payload in variants: + if not payload: + continue + unique_key = (payload.get("description"), payload.get("strategy"), label) + if unique_key in seen: + continue + seen.add(unique_key) + description = payload.get("description", "") + sentences = split_sentences(description) + sentence_set = set(sentences) + added = [item for item in sentences if item not in baseline_set][:3] + removed = [item for item in baseline_sentences if item not in sentence_set][:2] + dev_errors = payload.get("dev", {}).get("total_errors", 0) + holdout_errors = payload.get("holdout", {}).get("total_errors", 0) + cards.append( + { + "label": label, + "strategy": payload.get("strategy", "existing"), + "description": description, + "tokens": payload.get("estimated_tokens", 0), + "dev_errors": dev_errors, + "holdout_errors": holdout_errors, + "token_delta": metric_delta(payload.get("estimated_tokens", 0), baseline.get("estimated_tokens", 0)), + "dev_delta": metric_delta(dev_errors, baseline_dev), + "holdout_delta": metric_delta(holdout_errors, baseline_holdout), + "added": added if label != "Baseline" else baseline_sentences[:3], + "removed": removed, + } + ) + return cards + + def render_html(report: dict) -> str: overview = report["overview"] intent = report["intent"] @@ -146,6 +208,7 @@ def render_html(report: dict) -> str: architecture = architecture_steps(overview) compare_table_rows = compare_rows(compare) benchmark_rows = benchmark_cards(benchmark) + variant_cards = variant_diff_cards(compare) 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", [])) @@ -264,6 +327,38 @@ def render_html(report: dict) -> str: else: benchmark_html = "

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

    " + variant_diff_html = "" + if variant_cards: + variant_diff_html = "".join( + ( + "
    " + f"

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

    {html.escape(item['strategy'])}
    " + f"

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

    " + "
    " + f"tokens {html.escape(str(item['tokens']))} ({html.escape(item['token_delta'])})" + f"dev {html.escape(str(item['dev_errors']))} ({html.escape(item['dev_delta'])})" + f"holdout {html.escape(str(item['holdout_errors']))} ({html.escape(item['holdout_delta'])})" + "
    " + "
    " + "

    Adds relative to baseline

    " + + ( + "
      " + "".join(f"
    • {html.escape(value)}
    • " for value in item["added"]) + "
    " + if item["added"] + else "

    No added cues.

    " + ) + + "

    Drops from baseline

    " + + ( + "
      " + "".join(f"
    • {html.escape(value)}
    • " for value in item["removed"]) + "
    " + if item["removed"] + else "

    No dropped cues.

    " + ) + + "
    " + ) + for item in variant_cards + ) + else: + variant_diff_html = "

    No description optimization compare payload is attached yet.

    " + return f""" @@ -365,6 +460,12 @@ def render_html(report: dict) -> str: gap: 16px; margin-top: 16px; }} + .variant-grid {{ + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + margin-top: 16px; + }} .direction-card {{ padding: 18px; }} @@ -376,6 +477,48 @@ def render_html(report: dict) -> str: color: var(--muted); font-size: 13px; }} + .variant-card {{ + border: 1px solid var(--line); + background: var(--white); + padding: 18px; + }} + .variant-head {{ + display: flex; + justify-content: space-between; + gap: 12px; + align-items: baseline; + }} + .variant-head span {{ + color: var(--muted); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.06em; + }} + .variant-description {{ + margin: 14px 0; + padding-left: 14px; + border-left: 2px solid var(--line); + color: var(--text); + }} + .variant-metrics {{ + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 14px; + }} + .variant-metrics span {{ + border: 1px solid var(--line); + background: var(--soft); + padding: 6px 10px; + font-size: 12px; + }} + .variant-cues p {{ + margin: 8px 0 6px; + }} + .variant-cues ul {{ + margin: 0 0 12px; + padding-left: 18px; + }} table {{ width: 100%; border-collapse: collapse; @@ -395,7 +538,7 @@ def render_html(report: dict) -> str: color: var(--muted); }} @media (max-width: 1000px) {{ - .arch-grid, .direction-grid, .grid {{ + .arch-grid, .direction-grid, .variant-grid, .grid {{ grid-template-columns: 1fr; }} }} @@ -453,6 +596,11 @@ def render_html(report: dict) -> str: +
    +

    Variant diff studio

    +
    {variant_diff_html}
    +
    +

    Reference coach

    diff --git a/scripts/yao.py b/scripts/yao.py index 7dcebcaa..f2628d26 100644 --- a/scripts/yao.py +++ b/scripts/yao.py @@ -123,6 +123,13 @@ def prompt_with_default(label: str, default: str) -> str: return value or default +def prompt_optional(label: str, default: str = "skip") -> str: + sys.stderr.write(f"{label} [{default}]: ") + sys.stderr.flush() + value = sys.stdin.readline().strip() + return value or default + + def prompt_optional_entries(label: str) -> list[str]: sys.stderr.write(f"{label} [none]: ") sys.stderr.flush() @@ -173,6 +180,17 @@ def archetype_guidance(archetype: str) -> dict: return mapping.get(archetype, mapping["scaffold"]) +def discovery_summary(job: str, primary_output: str, archetype: str, guidance: dict) -> str: + return ( + "\nHere's the shape I'm hearing so far:\n" + f"- Repeated job: {job}\n" + f"- Desired hand-back: {primary_output}\n" + f"- Best starting archetype: {archetype}\n" + f"- First gate: {guidance['first_gate']}\n" + f"- Current focus: {guidance['focus']}\n" + ) + + def command_init(args: argparse.Namespace) -> int: cmd = [ args.name, @@ -203,27 +221,47 @@ def command_init(args: argparse.Namespace) -> int: def command_quickstart(args: argparse.Namespace) -> int: - 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") + sys.stderr.write("Let's start gently. You do not need a polished brief here.\n") + sys.stderr.write("Give me the real work in your own words, and I will help turn it into a clean first-pass skill.\n") + sys.stderr.write("Before we deepen the package, I will also look at a few strong public GitHub references so we can borrow patterns without copying them.\n") name = args.name or prompt_with_default("Skill name", "my-skill") - 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.") + job = args.job or prompt_with_default( + "In your own words, what repeated work do you most want this skill to quietly take over", + "Turn a repeated workflow into a reusable skill.", + ) + primary_output = args.primary_output or prompt_with_default( + "If it works beautifully, what should it hand back so you or the next person 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) + guidance = archetype_guidance(inferred_archetype) + sys.stderr.write(discovery_summary(job, primary_output, inferred_archetype, guidance)) + correction = prompt_optional( + "If I am off, what is the first thing I should correct before I package this idea", + "looks right", + ) + if correction.lower() not in {"looks right", "skip", "none", "no"}: + description = f"{description.rstrip('.')} Keep this correction in scope: {correction.rstrip('.')}." + inferred_archetype, archetype_reason = infer_archetype(job, description) + guidance = archetype_guidance(inferred_archetype) + sys.stderr.write("\nThanks. I tightened the frame before moving on.\n") + sys.stderr.write(discovery_summary(job, primary_output, inferred_archetype, guidance)) + archetype = args.archetype or prompt_with_default("I would start with this archetype (scaffold/production/library/governed)", inferred_archetype) archetype = archetype if archetype in ARCHETYPE_MODE else inferred_archetype default_mode = ARCHETYPE_MODE[archetype] - mode = args.mode or prompt_with_default("Mode (scaffold/production/library/governed)", default_mode) + mode = args.mode or prompt_with_default("For the first pass, I would keep the mode here (scaffold/production/library/governed)", default_mode) mode = mode if mode in ARCHETYPE_MODE.values() else default_mode + guidance = archetype_guidance(archetype) + sys.stderr.write( + f"\nGood. I will treat this as `{archetype}` in `{mode}` mode, so the first pass stays focused on {guidance['focus']}.\n" + ) user_references = args.user_reference or prompt_optional_entries( - "Your own reference examples to learn from as pattern hints (repo, product, page, workflow; comma-separated)" - ) - external_references = args.external_reference or prompt_optional_entries( - "Additional public benchmark objects you already know you want to borrow from (comma-separated)" + "If there is anything you admire and want me to learn from as pattern hints, send it here (repo, product, page, workflow; comma-separated)" ) + external_references = args.external_reference or [] local_constraints = args.local_constraint or prompt_optional_entries( - "Local constraints that must still be respected (privacy, naming, compatibility; comma-separated)" + "Tell me any local constraints I must keep in view (privacy, naming, compatibility; comma-separated)" ) 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") diff --git a/tests/verify_review_viewer.py b/tests/verify_review_viewer.py index 16978562..1542e219 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 "Variant diff studio" in html_text, html_text[:900] 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_yao_cli.py b/tests/verify_yao_cli.py index 54a3f437..846c2da0 100644 --- a/tests/verify_yao_cli.py +++ b/tests/verify_yao_cli.py @@ -50,7 +50,7 @@ def main() -> None: str(tmp_root), "--github-fixture-dir", str(BENCHMARK_FIXTURE_DIR), - input_text="quickstart-skill\nTurn rough notes into a reusable package.\nA reusable markdown workflow.\nproduction\nproduction\nA top-tier internal workflow product\nhigh-star GitHub repo, official docs\nprivacy and naming\n", + input_text="quickstart-skill\nTurn rough notes into a reusable package.\nA reusable markdown workflow.\nlooks right\nproduction\nproduction\nA top-tier internal workflow product\nprivacy and naming\n", ) assert quickstart_result["ok"], quickstart_result quickstart_root = Path(quickstart_result["payload"]["root"]) @@ -59,6 +59,7 @@ def main() -> None: assert quickstart_result["payload"]["archetype"] == "production", quickstart_result assert len(quickstart_result["payload"]["references"]["benchmark_repositories"]) == 3, quickstart_result assert quickstart_result["payload"]["references"]["user_references"] == ["A top-tier internal workflow product"], quickstart_result + assert quickstart_result["payload"]["guidance"]["experience_note"], quickstart_result validate_result = run("validate", str(created)) assert validate_result["ok"], validate_result