Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/web/app/components/templates/skill-template-gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,14 @@ function templateMatches(template: SkillTemplate, query: string): boolean {
template.category,
template.outcome,
template.autonomy,
template.userUseCase,
...template.personas,
...template.triggerModes,
...template.requiredApps.map((app) => app.name),
...template.interviewTopics,
...template.interviewQuestions.map((question) => question.prompt),
...template.interviewQuestions.flatMap((question) =>
question.options?.map((option) => `${option.label} ${option.description ?? ""}`) ?? [],
),
]
.join(" ")
.toLowerCase()
Expand Down
40 changes: 35 additions & 5 deletions apps/web/lib/skill-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,27 @@ describe("skill templates", () => {
expect(prompt).toContain("SKILL.md");
expect(prompt).toContain("Do not create or edit any files until");
expect(prompt).toContain("manual trigger and cron/scheduled agent messages");
expect(prompt).toContain("send rules");
expect(prompt).toContain("explicit send rules");
expect(prompt).toContain("Which buyer personas should DenchClaw");
expect(prompt).toContain("Who this skill is for");
expect(prompt).toContain("idempotency checks");
expect(prompt).toContain("Required external setup");
});

it("marks every template with UI metadata", () => {
it("marks every template with hand-authored UI and prompt metadata", () => {
const categories = new Set(SKILL_TEMPLATE_CATEGORIES);
const personas = new Set(SKILL_TEMPLATE_PERSONAS);
const ids = new Set<string>();
const instructionFingerprints = new Set<string>();

for (const template of SKILL_TEMPLATES) {
expect(ids.has(template.id)).toBe(false);
ids.add(template.id);
expect(categories.has(template.category)).toBe(true);
expect(template.outcome).toBeTruthy();
expect(template.summary).toBeTruthy();
expect(template.userUseCase).toBeTruthy();
expect(template.userUseCase.length).toBeGreaterThan(80);
expect(template.autonomy).toBeTruthy();
expect(template.personas.length).toBeGreaterThan(0);
for (const persona of template.personas) {
Expand All @@ -73,14 +78,39 @@ describe("skill templates", () => {
expect(app.name).not.toBe("Apollo");
}
expect(template.triggerModes.length).toBeGreaterThan(0);
expect(template.interviewTopics.length).toBeGreaterThan(3);
expect(template.skillInstructions.length).toBeGreaterThan(3);
expect(template.interviewQuestions.length).toBeGreaterThanOrEqual(4);
expect(template.interviewQuestions.length).toBeLessThanOrEqual(5);
for (const question of template.interviewQuestions) {
expect(question.id).toMatch(/^[a-z0-9-]+$/);
expect(question.prompt.length).toBeGreaterThan(24);
expect(typeof question.required).toBe("boolean");
if (question.options) {
expect(question.options.length).toBeGreaterThan(1);
for (const option of question.options) {
expect(option.id).toMatch(/^[a-z0-9-]+$/);
expect(option.label).toBeTruthy();
}
}
}
expect(template.skillInstructions.length).toBeGreaterThanOrEqual(6);
const allTemplateText = [
template.userUseCase,
...template.interviewQuestions.map((question) => question.prompt),
...template.skillInstructions,
].join("\n");
expect(allTemplateText).not.toContain("The exact ");
expect(allTemplateText).not.toContain("for this workflow");
expect(allTemplateText).not.toContain("A clear trigger description");
expect(allTemplateText).not.toContain("Step-by-step instructions to achieve this outcome");
const fingerprint = template.skillInstructions.join("|");
expect(instructionFingerprints.has(fingerprint)).toBe(false);
instructionFingerprints.add(fingerprint);
}

expect(ids.size).toBe(SKILL_TEMPLATES.length);
});

it("keeps each template definition in its own tiny file", () => {
it("keeps each template definition in its own file", () => {
for (const template of SKILL_TEMPLATES) {
const templateFile = fileURLToPath(
new URL(`./skill-templates/${template.id}.ts`, import.meta.url),
Expand Down
2 changes: 2 additions & 0 deletions apps/web/lib/skill-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export {
type SkillTemplateAutonomy,
type SkillTemplateCategory,
type SkillTemplateId,
type SkillTemplateInterviewQuestion,
type SkillTemplatePersona,
type SkillTemplateQuestionOption,
type SkillTemplateTriggerMode,
} from "./skill-templates/types";

Expand Down
82 changes: 79 additions & 3 deletions apps/web/lib/skill-templates/account-health-monitor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,89 @@
import { createSkillTemplate, externalApps } from "./create-template";
import { defineSkillTemplate, externalApps } from "./create-template";

export const accountHealthMonitor = createSkillTemplate({
export const accountHealthMonitor = defineSkillTemplate({
id: "account-health-monitor",
title: "Account Health Monitor",
summary: "Monitor accounts for risk, expansion signals, and missing next steps.",
category: "Grow Customers",
outcome: "Scores account health from CRM, email, meeting, and relationship signals, then alerts owners with recommended actions.",
userUseCase: "Use this when a founder or customer success owner wants a manual or scheduled monitor that turns scattered account activity into a reliable health view. The skill should synthesize CRM activity, enrichment, email, meetings, HubSpot, Slack, Notion, files, and web context into risk, expansion, relationship, and next-step signals.",
personas: ["Customer Success", "Founder"],
requiredApps: [externalApps.hubspot, externalApps.gmail, externalApps.slack],
triggerModes: ["scheduled", "manual"],
focusAreas: ["account segments", "health signals", "risk thresholds", "owner alerts"],
autonomy: "Updates CRM",
interviewQuestions: [
{
id: "account-scope",
prompt: "Which accounts should the health monitor cover?",
required: true,
allowMultiple: true,
options: [
{ id: "all-customers", label: "All customers" },
{ id: "strategic-accounts", label: "Strategic accounts" },
{ id: "paid-customers", label: "Paid customers" },
{ id: "implementation", label: "Implementation accounts" },
{ id: "at-risk", label: "Known at-risk accounts" },
],
freeformHint: "Include segment, lifecycle stage, ARR threshold, owner, or exclusion rules.",
},
{
id: "health-signals",
prompt: "Which signals should affect account health?",
required: true,
allowMultiple: true,
options: [
{ id: "engagement", label: "Engagement" },
{ id: "support-issues", label: "Support/issues" },
{ id: "executive-relationship", label: "Executive relationship" },
{ id: "usage-adoption", label: "Usage/adoption" },
{ id: "commercial-risk", label: "Commercial risk" },
{ id: "expansion-signal", label: "Expansion signal" },
],
},
{
id: "scoring-model",
prompt: "How should account health be reported?",
required: true,
options: [
{ id: "red-yellow-green", label: "Red/yellow/green" },
{ id: "numeric-score", label: "Numeric score" },
{ id: "ranked-risk-list", label: "Ranked risk list" },
{ id: "narrative-only", label: "Narrative only" },
],
},
{
id: "alert-thresholds",
prompt: "When should the skill alert an account owner?",
required: true,
allowMultiple: true,
options: [
{ id: "health-drops", label: "Health drops" },
{ id: "no-next-step", label: "No next step" },
{ id: "exec-silent", label: "Executive silent" },
{ id: "renewal-near", label: "Renewal near" },
{ id: "expansion-opportunity", label: "Expansion opportunity" },
],
},
{
id: "crm-write-policy",
prompt: "What CRM updates may the monitor make?",
required: true,
allowMultiple: true,
options: [
{ id: "health-note", label: "Add health note" },
{ id: "health-field", label: "Update health field" },
{ id: "owner-task", label: "Create owner task" },
{ id: "no-write", label: "Read-only digest" },
],
},
],
skillInstructions: [
"Use CRM, enrichment, web search, and files as the default source set; optionally incorporate Gmail, Calendar, HubSpot, Notion, and Slack when connected for recent activity and relationship context.",
"Support manual review runs and cron/scheduled health checks only; do not design around real-time triggers or webhook events.",
"Make scheduled runs idempotent by recording the account, run window, health outcome, alert reason, and created task IDs so unresolved alerts are not duplicated.",
"Apply the agreed scoring model consistently and include evidence, source links, confidence, and missing-data caveats for every health change.",
"Respect the CRM write policy: additive notes and attributed score updates are preferred; never overwrite owner-entered fields or lifecycle stages without explicit permission.",
"Alert account owners only when thresholds are met, with a short diagnosis, why it matters, the recommended next action, and whether the alert is urgent or informational.",
"Produce audience-specific outputs: owner action lists, founder-level portfolio summaries, CRM notes, and optional Slack or email digests in a direct founder-ops tone.",
],
});
78 changes: 75 additions & 3 deletions apps/web/lib/skill-templates/board-meeting-prep.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,85 @@
import { createSkillTemplate, externalApps } from "./create-template";
import { defineSkillTemplate, externalApps } from "./create-template";

export const boardMeetingPrep = createSkillTemplate({
export const boardMeetingPrep = defineSkillTemplate({
id: "board-meeting-prep",
title: "Board Meeting Prep",
summary: "Assemble board prep packets, open questions, risks, and follow-up actions.",
category: "Run Founder Ops",
outcome: "Gathers metrics, narrative updates, decisions needed, risks, and prior commitments into a board-ready brief.",
userUseCase:
"Use this when a founder or operator needs a repeatable way to assemble board prep from files, Notion, CRM, metrics, inbox context, meeting history, and prior commitments. The skill should create board-ready materials while keeping private founder notes separate from board-safe output.",
personas: ["Founder", "Operator"],
requiredApps: [externalApps.googleCalendar, externalApps.gmail, externalApps.notion],
triggerModes: ["manual", "scheduled"],
focusAreas: ["meeting date", "required sections", "metrics source", "sensitive topics"],
autonomy: "Updates CRM",
interviewQuestions: [
{
id: "meeting-context",
prompt: "What board meeting should this prep target?",
required: true,
freeformHint: "Include meeting date, board members, observers, format, and prep deadline.",
},
{
id: "packet-sections",
prompt: "Which sections should the board packet include?",
required: true,
allowMultiple: true,
options: [
{ id: "metrics", label: "Metrics" },
{ id: "wins", label: "Wins" },
{ id: "risks", label: "Risks" },
{ id: "decisions-needed", label: "Decisions needed" },
{ id: "hiring", label: "Hiring" },
{ id: "fundraising", label: "Fundraising" },
{ id: "customer-pipeline", label: "Customer/pipeline" },
],
},
{
id: "source-priority",
prompt: "Which sources should be treated as authoritative?",
required: true,
allowMultiple: true,
options: [
{ id: "files", label: "Files" },
{ id: "notion", label: "Notion" },
{ id: "crm", label: "Dench CRM" },
{ id: "hubspot", label: "HubSpot" },
{ id: "gmail-calendar", label: "Gmail/Calendar" },
{ id: "slack", label: "Slack" },
],
},
{
id: "audience-versions",
prompt: "Which audience-specific versions should be created?",
required: true,
allowMultiple: true,
options: [
{ id: "founder-private", label: "Founder private" },
{ id: "board-packet", label: "Board packet" },
{ id: "exec-team", label: "Exec team" },
{ id: "follow-up-list", label: "Follow-up list" },
],
},
{
id: "write-policy",
prompt: "What may be written back after prep?",
required: true,
allowMultiple: true,
options: [
{ id: "no-write", label: "No writes" },
{ id: "customer-risk-notes", label: "Customer risk notes" },
{ id: "owner-tasks", label: "Owner tasks" },
{ id: "board-followups", label: "Board follow-ups" },
],
},
],
skillInstructions: [
"Use Dench files, CRM, enrichment, and web search as default context; use Gmail, Calendar, Notion, HubSpot, and Slack for prior commitments, meeting logistics, source docs, and sensitive context when connected.",
"Support manual packet prep and scheduled prep reminders only; do not rely on calendar webhooks or automatic document callbacks.",
"Tie scheduled outputs to the board meeting date and packet version, updating the same draft or checklist instead of creating duplicates.",
"Keep private founder prep, exec-team action lists, and board-safe packets separate so sensitive working notes do not leak into external materials.",
"Respect the write policy: add sourced notes or tasks only when allowed, avoid overwriting CRM fields, and keep board-sensitive commentary out of customer-visible records.",
"Alert owners for missing metrics, unresolved prior board commitments, unclear decision owners, or customer/revenue risks that need executive input before the meeting.",
"Use a board-ready tone: clear, candid, numbers-aware, and explicit about decisions, tradeoffs, risks, and asks.",
],
});
68 changes: 65 additions & 3 deletions apps/web/lib/skill-templates/candidate-research-brief.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,75 @@
import { createSkillTemplate, externalApps } from "./create-template";
import { defineSkillTemplate, externalApps } from "./create-template";

export const candidateResearchBrief = createSkillTemplate({
export const candidateResearchBrief = defineSkillTemplate({
id: "candidate-research-brief",
title: "Candidate Research Brief",
summary: "Research candidates before outreach, interviews, or hiring debriefs.",
category: "Hire People",
outcome: "Creates candidate briefs with background, evidence, fit hypotheses, risks, and interview questions.",
userUseCase:
"Use this when a recruiter or founder needs a fair, cited candidate brief for outreach, interview prep, or debriefs. The skill should evaluate role-relevant evidence from candidate records, resumes, portfolios, public professional sources, and connected apps while avoiding protected-class and privacy-sensitive analysis.",
personas: ["Recruiter", "Founder"],
requiredApps: [externalApps.linkedin, externalApps.github],
triggerModes: ["manual"],
focusAreas: ["role requirements", "source links", "privacy rules", "brief format"],
autonomy: "Creates drafts",
interviewQuestions: [
{
id: "candidate",
prompt: "Which candidate should be researched?",
required: true,
freeformHint:
"Provide candidate name, CRM record, LinkedIn URL, resume, portfolio, GitHub profile, or uploaded files.",
},
{
id: "role",
prompt: "What role, level, and hiring criteria should the brief evaluate against?",
required: true,
freeformHint:
"Paste the job description, scorecard, must-haves, nice-to-haves, and interview stage.",
},
{
id: "allowed-sources",
prompt: "Which candidate sources may be used?",
required: true,
allowMultiple: true,
options: [
{ id: "crm", label: "Candidate record" },
{ id: "files", label: "Resume/files" },
{ id: "web", label: "Public web" },
{ id: "linkedin", label: "LinkedIn" },
{ id: "github", label: "GitHub/portfolio" },
],
},
{
id: "brief-purpose",
prompt: "What should the brief be used for?",
required: true,
options: [
{ id: "outreach", label: "Outreach" },
{ id: "interview-prep", label: "Interview prep" },
{ id: "debrief", label: "Debrief" },
{ id: "sourcing-fit", label: "Sourcing fit" },
],
},
{
id: "output-format",
prompt: "What output format should the candidate brief use?",
required: true,
options: [
{ id: "manual-brief", label: "Manual brief" },
{ id: "scorecard-prep", label: "Scorecard prep" },
{ id: "email-draft", label: "Outreach draft" },
{ id: "notion-note", label: "Notion note" },
],
freeformHint: "Include destination, audience, and whether outreach hooks should be included.",
},
],
skillInstructions: [
"Use only job-relevant, candidate-provided, public, or authorized internal information from Dench CRM, enrichment, files, and connected apps.",
"Do not infer, mention, score, or use protected-class or sensitive attributes such as age, race, religion, health, family status, gender identity, national origin, disability, veteran status, or photos.",
"Cite role-relevant claims with source references such as resume lines, portfolio pages, public work, CRM notes, or interview feedback.",
"Separate evidence from hypotheses and phrase fit as role-related observations or questions to validate.",
"Prefer primary candidate materials, work samples, public professional profiles, and authorized internal notes over low-quality web results.",
"Format the output as Candidate Snapshot, Role-Relevant Evidence, Fit Hypotheses, Risks/Unknowns, Suggested Interview Questions, Outreach Hooks if requested, and Sources.",
],
});
Loading