feat: add conversational quickstart and diff studio
This commit is contained in:
@@ -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"<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", []))
|
||||
@@ -264,6 +327,38 @@ def render_html(report: dict) -> str:
|
||||
else:
|
||||
benchmark_html = "<p class='minor'>No GitHub benchmark scan has been attached to this package yet.</p>"
|
||||
|
||||
variant_diff_html = ""
|
||||
if variant_cards:
|
||||
variant_diff_html = "".join(
|
||||
(
|
||||
"<div class='variant-card'>"
|
||||
f"<div class='variant-head'><h3>{html.escape(item['label'])}</h3><span>{html.escape(item['strategy'])}</span></div>"
|
||||
f"<p class='variant-description'>{html.escape(item['description'])}</p>"
|
||||
"<div class='variant-metrics'>"
|
||||
f"<span>tokens {html.escape(str(item['tokens']))} ({html.escape(item['token_delta'])})</span>"
|
||||
f"<span>dev {html.escape(str(item['dev_errors']))} ({html.escape(item['dev_delta'])})</span>"
|
||||
f"<span>holdout {html.escape(str(item['holdout_errors']))} ({html.escape(item['holdout_delta'])})</span>"
|
||||
"</div>"
|
||||
"<div class='variant-cues'>"
|
||||
"<p><strong>Adds relative to baseline</strong></p>"
|
||||
+ (
|
||||
"<ul>" + "".join(f"<li>{html.escape(value)}</li>" for value in item["added"]) + "</ul>"
|
||||
if item["added"]
|
||||
else "<p class='minor'>No added cues.</p>"
|
||||
)
|
||||
+ "<p><strong>Drops from baseline</strong></p>"
|
||||
+ (
|
||||
"<ul>" + "".join(f"<li>{html.escape(value)}</li>" for value in item["removed"]) + "</ul>"
|
||||
if item["removed"]
|
||||
else "<p class='minor'>No dropped cues.</p>"
|
||||
)
|
||||
+ "</div></div>"
|
||||
)
|
||||
for item in variant_cards
|
||||
)
|
||||
else:
|
||||
variant_diff_html = "<p class='minor'>No description optimization compare payload is attached yet.</p>"
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -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:
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Variant diff studio</h2>
|
||||
<div class="variant-grid">{variant_diff_html}</div>
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<div class="panel">
|
||||
<h2>Reference coach</h2>
|
||||
|
||||
+50
-12
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user