-
+ {/* 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 ? (
+
+
+ {/* 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. */}
+
+
+ {/* 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 ? (
+
@@ -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.