diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts index 0b08e80..da9eb58 100644 --- a/scripts/validate-skills.ts +++ b/scripts/validate-skills.ts @@ -189,6 +189,52 @@ Wrap values that contain \`: \` (colon + space), such as long descriptions with return { errors: [] } }, }, + { + description: 'Frontmatter "description" value must be wrapped in double quotes', + run({ dirName, rawFrontmatter }) { + if (rawFrontmatter === null) return { errors: [] } + const descLine = rawFrontmatter.split(/\r?\n/).find((l) => l.startsWith("description:")) + if (!descLine) return { errors: [] } + const rawValue = descLine.slice(descLine.indexOf(":") + 1).trim() + if (!rawValue.startsWith('"') || !rawValue.endsWith('"')) { + return { + errors: [ + `skills/${dirName}/SKILL.md: description value must be wrapped in double quotes — got: ${rawValue.slice(0, 60)}${rawValue.length > 60 ? "…" : ""}`, + ], + } + } + return { errors: [] } + }, + }, + { + description: + 'Special characters in description must be escaped (\\\\ and \\")', + run({ dirName, rawFrontmatter }) { + if (rawFrontmatter === null) return { errors: [] } + const descLine = rawFrontmatter.split(/\r?\n/).find((l) => l.startsWith("description:")) + if (!descLine) return { errors: [] } + const rawValue = descLine.slice(descLine.indexOf(":") + 1).trim() + if (!rawValue.startsWith('"') || !rawValue.endsWith('"')) return { errors: [] } + const inner = rawValue.slice(1, -1) + const issues: string[] = [] + // Strip valid escape sequences (\\, \") then check for remaining backslashes or unescaped quotes + const stripped = inner.replace(/\\\\|\\"/g, "") + if (stripped.includes('"')) { + issues.push('unescaped " (use \\")') + } + if (stripped.includes("\\")) { + issues.push("unescaped \\ (use \\\\)") + } + if (issues.length > 0) { + return { + errors: [ + `skills/${dirName}/SKILL.md: description contains ${issues.join(", ")}`, + ], + } + } + return { errors: [] } + }, + }, { description: "SKILL.md must have a non-empty body (instructions after the frontmatter block)", run({ dirName, body }) { @@ -236,6 +282,126 @@ Wrap values that contain \`: \` (colon + space), such as long descriptions with return { errors: [] } }, }, + { + description: "Metadata field must be a key-value map (not scalar or array)", + run({ dirName, rawFrontmatter }) { + if (rawFrontmatter === null) return { errors: [] } + const meta = parseMetadataBlock(rawFrontmatter) + if (meta === null) return { errors: [] } + if (meta === "scalar") { + return { + errors: [ + `skills/${dirName}/SKILL.md: "metadata" must be a key-value map, not an inline scalar — use indented sub-keys (e.g. metadata:\\n version: "1.0")`, + ], + } + } + if (meta === "list") { + return { + errors: [ + `skills/${dirName}/SKILL.md: "metadata" must be a key-value map, not a YAML list — use indented key-value pairs instead of "- " list items`, + ], + } + } + return { errors: [] } + }, + }, + { + description: 'Frontmatter must include a metadata block with a "version" field', + run({ dirName, rawFrontmatter }) { + if (rawFrontmatter === null) return { errors: [] } + const meta = parseMetadataBlock(rawFrontmatter) + + // Fail if no metadata block exists + if (meta === null) { + return { + errors: [ + `skills/${dirName}/SKILL.md: frontmatter must include a "metadata:" block with a "version" field (e.g. metadata:\\n version: "1.0")`, + ], + } + } + + // Fail if metadata is a scalar (inline value) + if (typeof meta === "string") { + return { + errors: [ + `skills/${dirName}/SKILL.md: metadata must be a key-value block, not an inline scalar — use indented sub-keys (e.g. metadata:\\n version: "1.0")`, + ], + } + } + + // Fail if version is missing inside metadata + if (!meta.version) { + return { + errors: [ + `skills/${dirName}/SKILL.md: metadata is missing required "version" field (e.g. version: "1.0")`, + ], + } + } + + return { errors: [] } + }, + }, + { + description: 'Version must follow x.y format (e.g. "1.0", "2.5")', + run({ dirName, rawFrontmatter }) { + if (rawFrontmatter === null) return { errors: [] } + const meta = parseMetadataBlock(rawFrontmatter) + if (meta === null || typeof meta === "string") return { errors: [] } + if (!meta.version) return { errors: [] } + const versionPattern = /^\d+\.\d+$/ + if (!versionPattern.test(meta.version)) { + return { + errors: [ + `skills/${dirName}/SKILL.md: version must follow x.y format (e.g. "1.0", "2.5") — got: "${meta.version}"`, + ], + } + } + return { errors: [] } + }, + }, + { + description: "Compatibility field (if present) must be at most 500 characters", + run({ dirName, frontmatter }) { + if (!frontmatter) return { errors: [] } + if (!("compatibility" in frontmatter)) return { errors: [] } + const len = frontmatter.compatibility?.length ?? 0 + if (len > 500) { + return { + errors: [`skills/${dirName}/SKILL.md: compatibility is ${len} characters (maximum 500)`], + } + } + return { errors: [] } + }, + }, + { + description: "Allowed-tools field (if present) must be a string (not array or object)", + run({ dirName, rawFrontmatter }) { + if (rawFrontmatter === null) return { errors: [] } + const line = rawFrontmatter.split(/\r?\n/).find((l) => l.startsWith("allowed-tools:")) + if (!line) return { errors: [] } + const rawValue = line.slice(line.indexOf(":") + 1).trim() + if (rawValue.startsWith("[") || rawValue.startsWith("{")) { + return { + errors: [ + `skills/${dirName}/SKILL.md: "allowed-tools" must be a plain string (space-separated tool names), not a YAML array or object — got: ${rawValue.slice(0, 60)}`, + ], + } + } + const nextLineIdx = rawFrontmatter.split(/\r?\n/).indexOf(line) + 1 + const lines = rawFrontmatter.split(/\r?\n/) + if (nextLineIdx < lines.length) { + const nextLine = lines[nextLineIdx] + if (nextLine.match(/^\s+- /)) { + return { + errors: [ + `skills/${dirName}/SKILL.md: "allowed-tools" must be a plain string, not a YAML list — use space-separated tool names (e.g. "Bash Read Write")`, + ], + } + } + } + return { errors: [] } + }, + }, { description: "Skill body should be under 500 lines for context efficiency", run({ dirName, body }) { @@ -251,6 +417,35 @@ Wrap values that contain \`: \` (colon + space), such as long descriptions with }, ] +/** + * Extracts nested key-value pairs from the `metadata:` block in raw frontmatter. + * Returns `null` if no metadata block, `"scalar"` if metadata has an inline value, + * `"list"` if it contains YAML list items, or a `Record` of sub-keys. + */ +function parseMetadataBlock(rawFrontmatter: string): Record | "scalar" | "list" | null { + const lines = rawFrontmatter.split(/\r?\n/) + const metaIdx = lines.findIndex((l) => /^metadata\s*:/.test(l)) + if (metaIdx === -1) return null + + const metaLine = lines[metaIdx] + const inlineValue = metaLine.slice(metaLine.indexOf(":") + 1).trim() + if (inlineValue && !inlineValue.startsWith("#")) return "scalar" + + const result: Record = {} + for (let i = metaIdx + 1; i < lines.length; i++) { + const line = lines[i] + if (!line.startsWith(" ") && !line.startsWith("\t")) break + const trimmed = line.trim() + if (trimmed.startsWith("- ")) return "list" + const colonIdx = trimmed.indexOf(":") + if (colonIdx === -1) continue + const key = trimmed.slice(0, colonIdx).trim() + const raw = trimmed.slice(colonIdx + 1).trim() + result[key] = raw.replace(/^(['"])([\s\S]*)\1$/, "$2") + } + return result +} + /** * Returns the deduplicated list of top-level skill directory names that have * changed relative to `base` (e.g. `origin/main`) and still exist on disk. diff --git a/skills/building-ui-bundle-frontend/SKILL.md b/skills/building-ui-bundle-frontend/SKILL.md index 372da9c..d6e837d 100644 --- a/skills/building-ui-bundle-frontend/SKILL.md +++ b/skills/building-ui-bundle-frontend/SKILL.md @@ -1,6 +1,8 @@ --- name: building-ui-bundle-frontend description: "MUST activate before editing ANY file under uiBundles/*/src/ for visual or UI changes to an EXISTING app — pages, components, sections, layout, styling, colors, fonts, navigation, animations, or any look-and-feel change. Use this skill when modifying pages, components, layout, styling, or navigation in an existing UI bundle app. Activate when the project contains appLayout.tsx, routes.tsx, src/pages/, src/components/, or global.css. This skill contains critical project-specific conventions (appLayout.tsx shell, shadcn/ui components, Tailwind CSS, Salesforce base-path routing, module restrictions) that override general knowledge. Without this skill, generated code will use wrong imports, break routing, or ignore project structure. Do NOT use when creating a new app from scratch (use building-ui-bundle-app instead)." +metadata: + version: "1.0" --- # UI Bundle UI diff --git a/skills/deploying-ui-bundle/SKILL.md b/skills/deploying-ui-bundle/SKILL.md index 7bff06a..b4f31f7 100644 --- a/skills/deploying-ui-bundle/SKILL.md +++ b/skills/deploying-ui-bundle/SKILL.md @@ -1,6 +1,8 @@ --- name: deploying-ui-bundle description: "MUST activate when the project contains a uiBundles/*/src/ directory or sfdx-project.json and the task involves deploying, pushing to an org, or post-deploy setup. Use this skill when deploying a UI bundle app to a Salesforce org. Covers the full deployment sequence: org authentication, pre-deploy build, metadata deployment, permission set assignment, data import, GraphQL schema fetch, and codegen. Activate when files like *.uibundle-meta.xml or sfdx-project.json exist and the user mentions deploying, pushing, org setup, or post-deploy tasks." +metadata: + version: "1.0" --- # Deploying a UI Bundle diff --git a/skills/developing-agentforce/SKILL.md b/skills/developing-agentforce/SKILL.md index 4b81779..31d9652 100644 --- a/skills/developing-agentforce/SKILL.md +++ b/skills/developing-agentforce/SKILL.md @@ -4,7 +4,7 @@ description: "Build, modify, debug, and deploy agents with Agentforce Agent Scri license: Apache-2.0 compatibility: "Requires Agentforce license, API v66.0+, Einstein Agent User" metadata: - version: "0.5.1" + version: "1.0" last_updated: "2026-04-08" --- diff --git a/skills/generating-apex-test/SKILL.md b/skills/generating-apex-test/SKILL.md index bbe360c..388d4a8 100644 --- a/skills/generating-apex-test/SKILL.md +++ b/skills/generating-apex-test/SKILL.md @@ -1,6 +1,8 @@ --- name: generating-apex-test -description: Generate and validate Apex test classes with TestDataFactory patterns, bulk testing (251+ records), mocking strategies, assertion best practices, and disciplined test-fix loops. Use this skill when creating new Apex test classes, improving test coverage, debugging and fixing failing Apex tests, running test execution and coverage analysis, or implementing testing patterns for triggers, services, controllers, batch jobs, queueables, and integrations. Triggers on *Test.cls, *_Test.cls files, sf apex run test workflows, coverage reports, test-fix loops. Do NOT trigger for production Apex code (use generating-apex) or Jest/LWC tests. +description: "Generate and validate Apex test classes with TestDataFactory patterns, bulk testing (251+ records), mocking strategies, assertion best practices, and disciplined test-fix loops. Use this skill when creating new Apex test classes, improving test coverage, debugging and fixing failing Apex tests, running test execution and coverage analysis, or implementing testing patterns for triggers, services, controllers, batch jobs, queueables, and integrations. Triggers on *Test.cls, *_Test.cls files, sf apex run test workflows, coverage reports, test-fix loops. Do NOT trigger for production Apex code (use generating-apex) or Jest/LWC tests." +metadata: + version: "1.0" --- # Generating Apex Tests diff --git a/skills/generating-apex/SKILL.md b/skills/generating-apex/SKILL.md index b18ba56..19416d2 100644 --- a/skills/generating-apex/SKILL.md +++ b/skills/generating-apex/SKILL.md @@ -1,6 +1,8 @@ --- name: generating-apex -description: Primary Apex authoring skill for class generation, refactoring, and review. ALWAYS ACTIVATE when the user mentions Apex, .cls, triggers, or asks to create/refactor a class (service, selector, domain, batch, queueable, schedulable, invocable, DTO, utility, interface, abstract, exception, REST resource). Use this skill for requests involving SObject CRUD, mapping collections, fetching related records, scheduled jobs, batch jobs, trigger design, @AuraEnabled controllers, @RestResource endpoints, custom REST APIs, or code review of existing Apex. +description: "Primary Apex authoring skill for class generation, refactoring, and review. ALWAYS ACTIVATE when the user mentions Apex, .cls, triggers, or asks to create/refactor a class (service, selector, domain, batch, queueable, schedulable, invocable, DTO, utility, interface, abstract, exception, REST resource). Use this skill for requests involving SObject CRUD, mapping collections, fetching related records, scheduled jobs, batch jobs, trigger design, @AuraEnabled controllers, @RestResource endpoints, custom REST APIs, or code review of existing Apex." +metadata: + version: "1.0" --- # Generating Apex @@ -403,4 +405,4 @@ Deploy: ## Troubleshooting Boundary -This skill handles production `.cls`/`.trigger`/`.apex` issues only: compile/parse failures, deployment dependency errors, runtime governor-limit failures. For test execution, assertions, coverage, or `sf apex run test` failures, delegate to `generating-apex-test`. \ No newline at end of file +This skill handles production `.cls`/`.trigger`/`.apex` issues only: compile/parse failures, deployment dependency errors, runtime governor-limit failures. For test execution, assertions, coverage, or `sf apex run test` failures, delegate to `generating-apex-test`. diff --git a/skills/generating-custom-application/SKILL.md b/skills/generating-custom-application/SKILL.md index 7d9e75b..bcb1a10 100644 --- a/skills/generating-custom-application/SKILL.md +++ b/skills/generating-custom-application/SKILL.md @@ -1,6 +1,8 @@ --- name: generating-custom-application description: "Use this skill when users need to create or configure Salesforce Custom Applications. Trigger when users mention custom apps, application metadata, app navigation, or organizing tabs into applications. Use when users want to create app containers for tabs and pages. Always use this skill for custom application work." +metadata: + version: "1.0" --- ## When to Use This Skill diff --git a/skills/generating-custom-field/SKILL.md b/skills/generating-custom-field/SKILL.md index 6eb9c6f..38c0246 100644 --- a/skills/generating-custom-field/SKILL.md +++ b/skills/generating-custom-field/SKILL.md @@ -1,6 +1,8 @@ --- name: generating-custom-field description: "Use this skill when users need to create, generate, or validate Salesforce Custom Field metadata. Trigger when users mention custom fields, field types, Roll-up Summary fields, Master-Detail relationships, Lookup relationships, formula fields, picklists, or field metadata. Also use when users encounter field deployment errors, especially around Roll-up Summary format, Master-Detail constraints, or formula issues. Always use this skill for any custom field metadata work, field generation, or field troubleshooting." +metadata: + version: "1.0" --- ## When to Use This Skill @@ -498,4 +500,4 @@ Before generating CustomField XML, verify: ### Naming Checks - [ ] Is the API name free of reserved words (`Order`, `Group`, `Select`, etc.)? -- [ ] Is the API name unique on this object? \ No newline at end of file +- [ ] Is the API name unique on this object? diff --git a/skills/generating-custom-lightning-type/SKILL.md b/skills/generating-custom-lightning-type/SKILL.md index 45511e0..bdf2fb1 100644 --- a/skills/generating-custom-lightning-type/SKILL.md +++ b/skills/generating-custom-lightning-type/SKILL.md @@ -1,6 +1,8 @@ --- name: generating-custom-lightning-type description: "Use this skill when users need to create Custom Lightning Types (CLTs) for Einstein Agent actions or structured input/output schemas. Trigger when users mention CLT, Custom Lightning Types, JSON schemas for agents, type definitions, lightning__objectType, or editor/renderer configurations. This is complex - always use this skill for CLT work." +metadata: + version: "1.0" --- ## When to Use This Skill diff --git a/skills/generating-custom-object/SKILL.md b/skills/generating-custom-object/SKILL.md index c4ac93d..99534e0 100644 --- a/skills/generating-custom-object/SKILL.md +++ b/skills/generating-custom-object/SKILL.md @@ -1,6 +1,8 @@ --- name: generating-custom-object description: "Use this skill when users need to create, generate, or validate Salesforce Custom Object metadata. Trigger when users mention custom objects, creating objects, object metadata, .object files, sharing models, name fields, or validation rules on objects. Also use when users say things like \"create a custom object\", \"generate object metadata\", \"set up an object for...\", or when they're troubleshooting object deployment errors especially around sharing models and Master-Detail relationships. Always use this skill for any custom object metadata work." +metadata: + version: "1.0" --- ## When to Use This Skill @@ -235,4 +237,4 @@ Before generating the Custom Object XML, verify: ### Architectural Checks - [ ] Is `` present with a meaningful summary? - [ ] Are `` and `` set to `true` if user-facing? -- [ ] Does the filename match the intended API name? \ No newline at end of file +- [ ] Does the filename match the intended API name? diff --git a/skills/generating-custom-tab/SKILL.md b/skills/generating-custom-tab/SKILL.md index 648b5b8..01c19d0 100644 --- a/skills/generating-custom-tab/SKILL.md +++ b/skills/generating-custom-tab/SKILL.md @@ -1,6 +1,8 @@ --- name: generating-custom-tab description: "Use this skill when users need to create or configure Salesforce Custom Tabs. Trigger when users mention tabs, navigation tabs, object tabs, web tabs, Visualforce tabs, Lightning component tabs, app page tabs, or tab configuration. Also use when users want to add navigation to custom objects, create tabs for external content, or set up Lightning page tabs. Always use this skill for any custom tab work." +metadata: + version: "1.0" --- ## When to Use This Skill @@ -151,4 +153,4 @@ Also forbidden: - Follow consistent naming conventions - Object tab files MUST only contain `true` and `` — nothing else - Web tab files MUST only contain: `false`, `