From 0c4133ef5bd48dc15cbcd3a8af7e439219237eff Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Thu, 14 May 2026 14:12:18 +0800 Subject: [PATCH] feat(agents): rewrite template catalog as 25 lightweight starters (#2587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(agents): rewrite template catalog as 25 lightweight starters Replaces every Phase-1 template with a curated set built around the "persona + intake + scaffold + hard negatives" instruction shape. Cross- platform survey (Cursor / Cline / Roo / Continue / Custom GPTs) showed the industry baseline for starter agents is "few but sharp" — single intent, no methodology buy-in, mostly prompt-only. The original catalog went the opposite direction (avg 2.5 skills, six-skill Full-stack methodology stack) and felt heavy for first-time use. Catalog shape: - 25 templates across 7 categories: Engineering (8), Product (4), Writing (5), Design (3), Communication (2), Team (1), Productivity (2). New Product / Design / Communication / Team domains fill gaps the old Eng-heavy catalog ignored. - 16 / 25 are prompt-only (no skill fan-out). Avg 0.56 skill per template vs. 2.5 prior. Heaviest is 2 skills, only for templates whose intent cannot be expressed in instructions alone (Playwright runner, single- file HTML bundlers, design + UX-guidelines pair). - Universal top-frequency intents that the old catalog missed are now covered: Code Explainer (intent #1 across every platform surveyed), Translator (中英), Summarizer, Writing Critic, PRD Drafter/Critic, RCA Writer, ADR Writer, PR Description Writer, Commit Message Writer. Loader allows 0-skill templates: - server/internal/agenttmpl/loader.go drops the "must declare at least one skill" validation; comment explains the picker's "Prompt only" rendering path. - loader_test.go: removed the corresponding negative case, added TestLoadFromFS_PromptOnlyTemplate as a regression guard. - agent_template.go handler is unchanged — every len(tmpl.Skills) call site was already 0-safe (empty fan-out short-circuits the fetch phase and the in-tx loop both skip cleanly). Frontend: - template-picker.tsx: 18 new lucide icons (BookOpen, Bug, GitPullRequest, GitCommit, AlertTriangle, Scale, ClipboardList, Microscope, UserRound, Target, Highlighter, Languages, AlignLeft, GraduationCap, Lightbulb, Type, MessageSquare, Briefcase). Card renders a "Prompt only" badge when skills.length === 0 instead of "0 skills". - template-detail.tsx: skill list section is hidden entirely for prompt- only templates — a header reading "Includes 0 skills" above an empty list was just visual noise. Instructions section below carries the agent's identity for these. - locales/en + zh-Hans agents.json: new create_dialog.template_card. prompt_only key ("Prompt only" / "纯指令"). Verification: - go test ./internal/agenttmpl/ — 9/9 pass, including TestLoad_RealTemplates which fails closed if any new JSON is malformed. - pnpm typecheck — all 6 packages clean. - pnpm --filter @multica/views test — 482/482 pass. - pnpm lint — 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(agents): add category filter pills to template picker 25 templates across 7 categories made the picker scroll-heavy on first open. Add a single-select category filter row above the grid so a PM can isolate Product templates in one click, an engineer can jump straight to Engineering, etc. Visual reuses the IssuesHeader scope-toggle pattern verbatim — Button variant="outline" + active class swap (bg-accent / text-muted-foreground) — so the affordance reads the same as the existing filter pills in issues / squads / runtimes / my-issues. flex-wrap keeps the 8 pills (All + 7 categories) honest on narrow widths. Counts are inlined into the label ("Engineering (8)") rather than shown as a separate badge — single-line-tall pills look right next to the picker grid, and surfacing the per-category density up front doubles as a hint at the catalog's "less but sharper" intent. When a specific category is active, the grid renders flat (no section headers) — the active pill already names what's on screen, and a header reading "Engineering" above an only-Engineering grid is visual duplication. "All" falls back to the prior grouped layout. State is component-local (no URL sync, no persistence) since the picker is dialog-internal transient state — closing the dialog naturally resets the filter, which is the expected behaviour for a "choose from a catalog" surface. i18n: new `create_dialog.template_picker.filter_all` key in en + zh. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .../agents/components/template-detail.tsx | 57 +++--- .../agents/components/template-picker.tsx | 183 +++++++++++++++--- packages/views/locales/en/agents.json | 6 +- packages/views/locales/zh-Hans/agents.json | 6 +- server/internal/agenttmpl/loader.go | 7 +- server/internal/agenttmpl/loader_test.go | 31 ++- .../agenttmpl/templates/adr-writer.json | 10 + .../agenttmpl/templates/article-writer.json | 21 -- .../agenttmpl/templates/brainstormer.json | 10 + .../agenttmpl/templates/bug-fixer.json | 16 ++ .../agenttmpl/templates/code-explainer.json | 10 + .../agenttmpl/templates/code-reviewer.json | 14 +- .../agenttmpl/templates/commit-message.json | 10 + .../agenttmpl/templates/docs-writer.json | 21 -- .../agenttmpl/templates/email-reply.json | 16 ++ .../agenttmpl/templates/frontend-builder.json | 6 +- .../templates/frontend-designer.json | 11 +- .../templates/full-stack-engineer.json | 41 ---- .../agenttmpl/templates/html-slides.json | 7 +- .../agenttmpl/templates/internal-comms.json | 21 -- .../agenttmpl/templates/jd-writer.json | 10 + .../agenttmpl/templates/okr-drafter.json | 10 + .../agenttmpl/templates/one-pager.json | 7 +- .../internal/agenttmpl/templates/planner.json | 31 --- .../agenttmpl/templates/pr-description.json | 10 + .../agenttmpl/templates/prd-critic.json | 10 + .../agenttmpl/templates/prd-drafter.json | 10 + .../agenttmpl/templates/rca-writer.json | 10 + .../agenttmpl/templates/release-notes.json | 10 + .../agenttmpl/templates/summarizer.json | 10 + .../agenttmpl/templates/translator-zh-en.json | 10 + .../internal/agenttmpl/templates/tutor.json | 10 + .../templates/user-story-writer.json | 10 + .../agenttmpl/templates/ux-copywriter.json | 10 + .../agenttmpl/templates/webapp-tester.json | 4 +- .../agenttmpl/templates/writing-critic.json | 10 + 36 files changed, 437 insertions(+), 239 deletions(-) create mode 100644 server/internal/agenttmpl/templates/adr-writer.json delete mode 100644 server/internal/agenttmpl/templates/article-writer.json create mode 100644 server/internal/agenttmpl/templates/brainstormer.json create mode 100644 server/internal/agenttmpl/templates/bug-fixer.json create mode 100644 server/internal/agenttmpl/templates/code-explainer.json create mode 100644 server/internal/agenttmpl/templates/commit-message.json delete mode 100644 server/internal/agenttmpl/templates/docs-writer.json create mode 100644 server/internal/agenttmpl/templates/email-reply.json delete mode 100644 server/internal/agenttmpl/templates/full-stack-engineer.json delete mode 100644 server/internal/agenttmpl/templates/internal-comms.json create mode 100644 server/internal/agenttmpl/templates/jd-writer.json create mode 100644 server/internal/agenttmpl/templates/okr-drafter.json delete mode 100644 server/internal/agenttmpl/templates/planner.json create mode 100644 server/internal/agenttmpl/templates/pr-description.json create mode 100644 server/internal/agenttmpl/templates/prd-critic.json create mode 100644 server/internal/agenttmpl/templates/prd-drafter.json create mode 100644 server/internal/agenttmpl/templates/rca-writer.json create mode 100644 server/internal/agenttmpl/templates/release-notes.json create mode 100644 server/internal/agenttmpl/templates/summarizer.json create mode 100644 server/internal/agenttmpl/templates/translator-zh-en.json create mode 100644 server/internal/agenttmpl/templates/tutor.json create mode 100644 server/internal/agenttmpl/templates/user-story-writer.json create mode 100644 server/internal/agenttmpl/templates/ux-copywriter.json create mode 100644 server/internal/agenttmpl/templates/writing-critic.json diff --git a/packages/views/agents/components/template-detail.tsx b/packages/views/agents/components/template-detail.tsx index dc64287fc..3abb0f992 100644 --- a/packages/views/agents/components/template-detail.tsx +++ b/packages/views/agents/components/template-detail.tsx @@ -145,32 +145,37 @@ export function TemplateDetail({ /> - {/* Skill list — always visible (summary has cached descriptions) */} -
-

- {t(($) => $.create_dialog.template_detail.skill_count, { - count: template.skills.length, - })} -

-
    - {template.skills.map((s) => ( -
  • -
    - - {s.cached_name} -
    - {s.cached_description ? ( -

    - {s.cached_description} -

    - ) : null} -
  • - ))} -
-
+ {/* Skill list — hidden entirely for prompt-only templates. Most of + the catalog is 0-skill; a section reading "Includes 0 skills" + with an empty list would just be visual noise. The Instructions + section below carries the agent's identity for these. */} + {template.skills.length > 0 ? ( +
+

+ {t(($) => $.create_dialog.template_detail.skill_count, { + count: template.skills.length, + })} +

+
    + {template.skills.map((s) => ( +
  • +
    + + {s.cached_name} +
    + {s.cached_description ? ( +

    + {s.cached_description} +

    + ) : null} +
  • + ))} +
+
+ ) : null} {/* Instructions — lazy fetch + loading/error states */}
diff --git a/packages/views/agents/components/template-picker.tsx b/packages/views/agents/components/template-picker.tsx index e56faa571..b94c4ecf0 100644 --- a/packages/views/agents/components/template-picker.tsx +++ b/packages/views/agents/components/template-picker.tsx @@ -1,25 +1,44 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { + AlertTriangle, + AlignLeft, + BookOpen, Brush, + Briefcase, + Bug, ChevronRight, + ClipboardList, FileText, FlaskConical, + GitCommit, + GitPullRequest, + GraduationCap, + Highlighter, + Languages, LayoutDashboard, + Lightbulb, ListChecks, Loader2, Megaphone, + MessageSquare, + Microscope, Palette, PenLine, Presentation, + Scale, Search, Sparkles, + Target, + Type, + UserRound, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { agentTemplateListOptions } from "@multica/core/agents/queries"; import type { AgentTemplateSummary } from "@multica/core/types"; +import { Button } from "@multica/ui/components/ui/button"; import { cn } from "@multica/ui/lib/utils"; import { useT } from "../../i18n"; @@ -50,6 +69,11 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) { agentTemplateListOptions(), ); + // `null` = "All" (default). When a specific category is selected, the + // grid renders flat (no section headers) — the active pill already + // tells the user what they're looking at, so headers would be noise. + const [selectedCategory, setSelectedCategory] = useState(null); + // Group by category. Templates without a category fall into the // localised "Other" bucket so they still render. Preserves the load // order within each group for deterministic UI (matches the @@ -65,6 +89,18 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) { return Array.from(byCategory.entries()); }, [templates, otherCategory]); + // Templates currently visible given the filter. When "All" is active + // we show every template (grouped by category below); otherwise we + // only show the matching category. + const visibleTemplates = useMemo(() => { + if (selectedCategory === null) return templates; + return templates.filter( + (tmpl) => + (tmpl.category?.trim() ? tmpl.category : otherCategory) === + selectedCategory, + ); + }, [templates, selectedCategory, otherCategory]); + if (isLoading) { return (
@@ -95,28 +131,97 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) { return (
-
- {groups.map(([category, tmpls]) => ( -
-

- {category} -

-
- {tmpls.map((tmpl) => ( - onSelect(tmpl)} - /> - ))} -
-
- ))} +
+ {/* Category filter — mirrors the IssuesHeader scope pattern + (Button variant="outline" + active-class swap). `flex-wrap` + so the 8 pills (All + 7 categories) degrade gracefully on + narrow widths. Counts are inlined into the label rather than + shown as a separate badge because we want the pill row to + stay one-line-tall per pill. */} +
+ $.create_dialog.template_picker.filter_all)} (${templates.length})`} + active={selectedCategory === null} + onClick={() => setSelectedCategory(null)} + /> + {groups.map(([category, tmpls]) => ( + setSelectedCategory(category)} + /> + ))} +
+ + {/* Grid — grouped with sticky headers when "All" is active; + flat when a single category is filtered (the active pill + already tells the user what they're looking at). */} + {selectedCategory === null ? ( +
+ {groups.map(([category, tmpls]) => ( +
+

+ {category} +

+
+ {tmpls.map((tmpl) => ( + onSelect(tmpl)} + /> + ))} +
+
+ ))} +
+ ) : ( +
+ {visibleTemplates.map((tmpl) => ( + onSelect(tmpl)} + /> + ))} +
+ )}
); } +/** Single filter pill. Visual matches IssuesHeader's scope toggle + * (Button outline + bg-accent on active) so the catalog feels + * consistent with the rest of the app's filter affordances. */ +function FilterPill({ + label, + active, + onClick, +}: { + label: string; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} + interface TemplateCardProps { template: AgentTemplateSummary; onClick: () => void; @@ -150,9 +255,11 @@ function TemplateCard({ template, onClick }: TemplateCardProps) { {template.description}

- {t(($) => $.create_dialog.template_card.skills, { - count: template.skills.length, - })} + {template.skills.length === 0 + ? t(($) => $.create_dialog.template_card.prompt_only) + : t(($) => $.create_dialog.template_card.skills, { + count: template.skills.length, + })}
@@ -164,17 +271,35 @@ function TemplateCard({ template, onClick }: TemplateCardProps) { /** Lucide icon name → component. Add new entries when shipping templates * that use icons not yet listed here. Unknown names fall back to FileText. */ const ICONS: Record = { - Search, - Palette, + AlertTriangle, + AlignLeft, + BookOpen, + Briefcase, + Brush, + Bug, + ClipboardList, FileText, FlaskConical, - Sparkles, - ListChecks, - Brush, - PenLine, - Megaphone, - Presentation, + GitCommit, + GitPullRequest, + GraduationCap, + Highlighter, + Languages, LayoutDashboard, + Lightbulb, + ListChecks, + Megaphone, + MessageSquare, + Microscope, + Palette, + PenLine, + Presentation, + Scale, + Search, + Sparkles, + Target, + Type, + UserRound, }; /** Semantic accent → Tailwind class string. The class strings are written diff --git a/packages/views/locales/en/agents.json b/packages/views/locales/en/agents.json index 308a4f96c..a6cde0c92 100644 --- a/packages/views/locales/en/agents.json +++ b/packages/views/locales/en/agents.json @@ -235,11 +235,13 @@ "title": "Pick a template", "empty": "No templates available yet.", "load_failed": "Failed to load templates", - "other_category": "Other" + "other_category": "Other", + "filter_all": "All" }, "template_card": { "skills_one": "{{count}} skill", - "skills_other": "{{count}} skills" + "skills_other": "{{count}} skills", + "prompt_only": "Prompt only" }, "template_detail": { "load_failed": "Failed to load template", diff --git a/packages/views/locales/zh-Hans/agents.json b/packages/views/locales/zh-Hans/agents.json index c7bb3807b..2b8404c20 100644 --- a/packages/views/locales/zh-Hans/agents.json +++ b/packages/views/locales/zh-Hans/agents.json @@ -230,10 +230,12 @@ "title": "选择模板", "empty": "暂无可用模板。", "load_failed": "加载模板失败", - "other_category": "其他" + "other_category": "其他", + "filter_all": "全部" }, "template_card": { - "skills_other": "{{count}} 个 skill" + "skills_other": "{{count}} 个 skill", + "prompt_only": "纯指令" }, "template_detail": { "load_failed": "加载模板失败", diff --git a/server/internal/agenttmpl/loader.go b/server/internal/agenttmpl/loader.go index 0efa05029..dc19ab34d 100644 --- a/server/internal/agenttmpl/loader.go +++ b/server/internal/agenttmpl/loader.go @@ -100,9 +100,10 @@ func validate(t Template, filename string) error { if strings.TrimSpace(t.Instructions) == "" { return fmt.Errorf("missing instructions") } - if len(t.Skills) == 0 { - return fmt.Errorf("must declare at least one skill") - } + // 0-skill templates are legitimate — most starter templates are + // prompt-only (instructions alone, no skill fan-out). See + // docs/agent-quick-create-plan.md and the picker UI's "Prompt only" + // rendering for zero-length skill arrays. for i, s := range t.Skills { if strings.TrimSpace(s.SourceURL) == "" { return fmt.Errorf("skill[%d]: missing source_url", i) diff --git a/server/internal/agenttmpl/loader_test.go b/server/internal/agenttmpl/loader_test.go index 68e6139a8..44b925863 100644 --- a/server/internal/agenttmpl/loader_test.go +++ b/server/internal/agenttmpl/loader_test.go @@ -92,11 +92,6 @@ func TestLoadFromFS_Invalid(t *testing.T) { content: `{"slug":"x","name":"X","skills":[{"source_url":"u"}]}`, wantErr: "missing instructions", }, - { - name: "no skills", - content: `{"slug":"x","name":"X","instructions":"do","skills":[]}`, - wantErr: "at least one skill", - }, { name: "skill missing url", content: `{"slug":"x","name":"X","instructions":"do","skills":[{}]}`, @@ -124,6 +119,32 @@ func TestLoadFromFS_Invalid(t *testing.T) { } } +func TestLoadFromFS_PromptOnlyTemplate(t *testing.T) { + // 0-skill templates are legitimate. Most starter templates ship prompt-only + // (no skill fan-out, just an instructions block). Regression guard so the + // "must declare at least one skill" rule doesn't sneak back in. + fsys := fstest.MapFS{ + "templates/prompt-only.json": &fstest.MapFile{Data: []byte(`{ + "slug":"prompt-only", + "name":"Prompt Only", + "description":"no skills here", + "instructions":"just write good prose", + "skills":[] + }`)}, + } + reg, err := loadFromFS(fsys, "templates") + if err != nil { + t.Fatalf("loadFromFS: %v", err) + } + tmpl, ok := reg.Get("prompt-only") + if !ok { + t.Fatal("Get(prompt-only) = false, want true") + } + if len(tmpl.Skills) != 0 { + t.Errorf("len(Skills) = %d, want 0", len(tmpl.Skills)) + } +} + func TestLoadFromFS_DuplicateSlug(t *testing.T) { // Two valid files declaring the same slug — caught by the registry, not // by validate(). Slugs are unique within the registry. diff --git a/server/internal/agenttmpl/templates/adr-writer.json b/server/internal/agenttmpl/templates/adr-writer.json new file mode 100644 index 000000000..2ed808d2c --- /dev/null +++ b/server/internal/agenttmpl/templates/adr-writer.json @@ -0,0 +1,10 @@ +{ + "slug": "adr-writer", + "name": "ADR Writer", + "description": "Captures an architecture decision in the standard Context / Decision / Consequences format — so future-you knows why.", + "category": "Engineering", + "icon": "Scale", + "accent": "info", + "instructions": "You write Architecture Decision Records. Reader: an engineer joining the team in a year who needs to understand why the system looks the way it does — not just what it does.\n\nFixed scaffold:\n\n```\n# ADR-NNN: \n\n## Status\nProposed | Accepted | Superseded by ADR-XXX | Deprecated\n\n## Context\n2-4 sentences. The forces in play: what the system is doing today, what need has emerged, what constraints we must respect, why now. Not the answer — the question.\n\n## Decision\n1-3 sentences. The decision in active voice. \"We will use X\" — not \"Adoption of X is recommended\". Be specific enough that a reader can implement it without a follow-up meeting.\n\n## Alternatives considered\n- **Option B** — one-sentence rejection reason (e.g. \"adds runtime overhead we can't afford\")\n- **Option C** — one-sentence rejection reason\n\n## Consequences\n- **Positive:** what gets easier, faster, or possible\n- **Negative:** what gets harder, slower, or impossible (be honest — burying these is how teams build up resentment)\n- **Neutral / open:** assumptions we're making, things we'll need to revisit\n```\n\nDefaults:\n\n1. **One decision per ADR.** If you find yourself writing \"we also decided to ...\", stop — that's a second ADR.\n2. **Name 1-3 alternatives, no more.** An ADR with no rejected options reads like there was no real choice. An ADR comparing 8 options reads like a survey.\n3. **Consequences are not all positive.** Include the things this decision makes harder. The reader needs to know what they're trading away.\n4. **Keep it short.** Most ADRs fit on one screen. If yours is two pages, you're explaining the system, not the decision.\n5. **Match the repo's existing ADR numbering and prose style** if the user shows you prior ADRs.\n\nDo NOT: include code samples (link the implementation PR instead); write generic platitudes (\"this is good for maintainability\"); skip the Status field; use marketing language (\"best-in-class\", \"future-proof\"); leave alternatives blank to avoid awkward conversation.", + "skills": [] +} diff --git a/server/internal/agenttmpl/templates/article-writer.json b/server/internal/agenttmpl/templates/article-writer.json deleted file mode 100644 index 8a35670f7..000000000 --- a/server/internal/agenttmpl/templates/article-writer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "slug": "article-writer", - "name": "Article Writer", - "description": "Drafts longform articles, blog posts, and release notes that sound like a person, not a content farm.", - "category": "Writing", - "icon": "PenLine", - "accent": "success", - "instructions": "You write longform prose — blog posts, release notes, deep-dives, engineering write-ups. Defaults:\n\n1. **Find the angle before the outline.** What does the reader walk away knowing that they didn't before? If you can't name it in one sentence, you don't have a piece yet — push back on the brief.\n2. **Open with the thing that matters.** First paragraph either states the surprising result, the concrete problem, or the question you're answering. No \"in today's fast-paced world\", no throat-clearing, no \"I'm excited to share\".\n3. **Use the doc-coauthoring skill's structured workflow.** Draft → self-edit for clarity → tighten → check against tone. Each pass has a single goal; do not blur them.\n4. **Apply the brand-guidelines skill for voice and terminology.** Names, capitalization, product nouns must match. Don't invent synonyms because variety \"reads better\".\n5. **Concrete over abstract.** Every claim gets a number, an example, or a code snippet. Replace adjectives with evidence. \"Faster\" → \"3.2x faster on the 10k-row case\".\n6. **End with what to do next.** Not a CTA, a next step — what the reader does on Monday morning if they buy your argument.\n\nDo NOT: write marketing copy, listicle filler, or \"Top 10 X You Should Know About\". Avoid em-dash overuse. Avoid \"in conclusion\" and \"that being said\". One-sentence paragraphs are allowed when they earn their line.", - "skills": [ - { - "source_url": "https://github.com/anthropics/skills/tree/main/skills/doc-coauthoring", - "cached_name": "doc-coauthoring", - "cached_description": "Structured workflow for co-authoring longform structured content." - }, - { - "source_url": "https://github.com/anthropics/skills/tree/main/skills/brand-guidelines", - "cached_name": "brand-guidelines", - "cached_description": "Apply consistent brand voice, terminology, and style." - } - ] -} diff --git a/server/internal/agenttmpl/templates/brainstormer.json b/server/internal/agenttmpl/templates/brainstormer.json new file mode 100644 index 000000000..dac433dcf --- /dev/null +++ b/server/internal/agenttmpl/templates/brainstormer.json @@ -0,0 +1,10 @@ +{ + "slug": "brainstormer", + "name": "Brainstormer", + "description": "Generates 15-20 wide-ranging options — names, variants, ideas, angles — without prematurely converging.", + "category": "Writing", + "icon": "Lightbulb", + "accent": "primary", + "instructions": "You generate options. Lots of them. Your job is divergence — narrowing happens later (and not by you).\n\nDefaults:\n\n1. **Default to 15-20 options.** Quantity beats quality at the brainstorm stage. The best idea is rarely #1 or #2; it's #11 with #4's twist.\n2. **Span the design space deliberately.** Don't generate 20 variants of one direction. Group your output: literal / metaphorical / contrarian / formal / playful / niche / off-the-wall. Cover at least 4 of those groups.\n3. **Label each option with its angle.** One short tag in parens: `(literal)`, `(metaphor)`, `(contrarian)`, `(constraint inversion)`, etc. The user picks not just an idea but a direction.\n4. **Include one or two deliberately bad ones.** A clearly-too-far option (`(deliberately too far)`) widens the user's sense of the space. It also makes the rest land better.\n5. **No premature filtering.** Don't pre-justify why an option is or isn't the right one. The user judges. You produce.\n\nIntake one question before generating:\n- What's the brief? (One sentence: what you want options for.)\n- Any hard constraints? (Length, tone, audience, formats to avoid.)\n- What direction has already been tried and rejected? (So you don't repeat.)\n\nOutput shape:\n\n```\n**Brief**: \n\n1.