From 2320c5026469d974cddc202d37a82938fdaa404d Mon Sep 17 00:00:00 2001 From: Mohan Raj Rajamanickam <1509984+mohanraj-r@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:41:52 -0700 Subject: [PATCH] ci: publish skills as @salesforce/afv-skills npm package @W-21534193@ (#30) * ci: update package details and add GitHub workflows for skills validation and release * chore: update Node version, change license, and refactor skills validation script to TypeScript * chore: remove unused deps * fix: npm package * fix: flatten skills to pass validation * chore: add validation script to package.json and update GitHub workflow to use it * chore: enhance skills validation and update workflows for npm publishing * refactor: streamline skills validation process in workflow and script * refactor: improve documentation and structure of skills validation script * implement checks based on jeff's best practices doc * chore: add pull request template for skill submissions * chore: update pull request template with additional references and improve automated checks section * chore: update pull request template to clarify naming convention with a warning for gerund form * chore: update pull request template to reference skill authoring guide and enhance checklist structure * chore: enhance GitHub workflows for skills validation and release process * refactor: improve parsing logic for SKILL.md files and enhance error collection in validation * chore: update validation checks to enforce character limits for skill names and descriptions * fix: remove unnecessary whitespace in footer string in validate-skills.ts --- .github/pull_request_template.md | 43 ++ .github/workflows/release-skills.yml | 91 +++ .github/workflows/validate-pr.yml | 10 + .github/workflows/validate-skills.yml | 36 ++ .nvmrc | 2 +- package-lock.json | 552 +++++++++++++++++- package.json | 16 +- scripts/validate-skills.ts | 368 ++++++++++++ .../apex-class/SKILL.md | 0 .../examples/AccountDeduplicationBatch.cls | 0 .../apex-class/examples/AccountSelector.cls | 0 .../apex-class/examples/AccountService.cls | 0 .../apex-class/templates/abstract.cls | 0 .../apex-class/templates/batch.cls | 0 .../apex-class/templates/domain.cls | 0 .../apex-class/templates/dto.cls | 0 .../apex-class/templates/exception.cls | 0 .../apex-class/templates/interface.cls | 0 .../apex-class/templates/queueable.cls | 0 .../apex-class/templates/schedulable.cls | 0 .../apex-class/templates/selector.cls | 0 .../apex-class/templates/service.cls | 0 .../apex-class/templates/utility.cls | 0 .../apex-test-class/SKILL.md | 0 .../references/assertion-patterns.md | 0 .../references/async-testing.md | 0 .../references/mocking-patterns.md | 0 .../references/test-data-factory.md | 0 .../deployment-readiness-check/SKILL.md | 0 .../assets/deployment_checklist.md | 0 .../references/rollback_procedures.md | 0 .../scripts/check_metadata.sh | 0 .../trigger-refactor-pipeline/SKILL.md | 0 .../assets/test_template.apex | 0 .../references/handler_patterns.md | 0 .../scripts/analyze_trigger.py | 0 36 files changed, 1111 insertions(+), 7 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/release-skills.yml create mode 100644 .github/workflows/validate-pr.yml create mode 100644 .github/workflows/validate-skills.yml create mode 100644 scripts/validate-skills.ts rename skills/{apex-development => }/apex-class/SKILL.md (100%) rename skills/{apex-development => }/apex-class/examples/AccountDeduplicationBatch.cls (100%) rename skills/{apex-development => }/apex-class/examples/AccountSelector.cls (100%) rename skills/{apex-development => }/apex-class/examples/AccountService.cls (100%) rename skills/{apex-development => }/apex-class/templates/abstract.cls (100%) rename skills/{apex-development => }/apex-class/templates/batch.cls (100%) rename skills/{apex-development => }/apex-class/templates/domain.cls (100%) rename skills/{apex-development => }/apex-class/templates/dto.cls (100%) rename skills/{apex-development => }/apex-class/templates/exception.cls (100%) rename skills/{apex-development => }/apex-class/templates/interface.cls (100%) rename skills/{apex-development => }/apex-class/templates/queueable.cls (100%) rename skills/{apex-development => }/apex-class/templates/schedulable.cls (100%) rename skills/{apex-development => }/apex-class/templates/selector.cls (100%) rename skills/{apex-development => }/apex-class/templates/service.cls (100%) rename skills/{apex-development => }/apex-class/templates/utility.cls (100%) rename skills/{testing-automation => }/apex-test-class/SKILL.md (100%) rename skills/{testing-automation => }/apex-test-class/references/assertion-patterns.md (100%) rename skills/{testing-automation => }/apex-test-class/references/async-testing.md (100%) rename skills/{testing-automation => }/apex-test-class/references/mocking-patterns.md (100%) rename skills/{testing-automation => }/apex-test-class/references/test-data-factory.md (100%) rename skills/{metadata-deployments => }/deployment-readiness-check/SKILL.md (100%) rename skills/{metadata-deployments => }/deployment-readiness-check/assets/deployment_checklist.md (100%) rename skills/{metadata-deployments => }/deployment-readiness-check/references/rollback_procedures.md (100%) rename skills/{metadata-deployments => }/deployment-readiness-check/scripts/check_metadata.sh (100%) rename skills/{apex-development => }/trigger-refactor-pipeline/SKILL.md (100%) rename skills/{apex-development => }/trigger-refactor-pipeline/assets/test_template.apex (100%) rename skills/{apex-development => }/trigger-refactor-pipeline/references/handler_patterns.md (100%) rename skills/{apex-development => }/trigger-refactor-pipeline/scripts/analyze_trigger.py (100%) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..bc19414 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,43 @@ +**References:** [Contributing guide](../CONTRIBUTING.md) · [Skill authoring guide](../README.md) · [Agent Skills spec](https://agentskills.io/specification) + +## What changed + + + +## Why + + + +## Notes + + + +--- + +## Skills + +### Manual checklist + +**Description quality** +- [ ] Describes what the skill does and the expected output +- [ ] Includes relevant Salesforce domain keywords (Apex, LWC, SOQL, metadata types, etc.) +- [ ] Trigger phrases are specific enough for Vibes to select this skill reliably + +**Instructions** +- [ ] Clear goal statement +- [ ] Step-by-step workflow +- [ ] Validation rules for generated output +- [ ] Defined output / artifact + +**Context efficiency** +- [ ] Core instructions are concise — supporting material lives in `templates/`, `examples/`, or `docs/` subdirectories +- [ ] No unnecessary background explanation in the body + +### Automated checks + +Enforced by CI ([`npm run validate:skills`](../scripts/validate-skills.ts)) per the [Agent Skills spec](https://agentskills.io/specification): + +- Directory is one level deep, named in kebab-case (max 64 chars), contains `SKILL.md` +- Frontmatter `name` matches directory name; `description` is present, ≥ 20 words, ≤ 1024 characters, and includes trigger language +- Body is non-empty and under 500 lines +- Name uses gerund form ⚠ (warning — does not block merge) diff --git a/.github/workflows/release-skills.yml b/.github/workflows/release-skills.yml new file mode 100644 index 0000000..3cd55bd --- /dev/null +++ b/.github/workflows/release-skills.yml @@ -0,0 +1,91 @@ +name: release-skills + +on: + push: + branches: [main] + paths: ['skills/**'] + workflow_dispatch: + inputs: + ref: + description: "Branch or tag to release from (must be main)" + required: false + default: main + +permissions: + contents: write + packages: write + +jobs: + release: + runs-on: ubuntu-latest + outputs: + skipped: ${{ steps.changelog.outputs.skipped }} + version: ${{ steps.changelog.outputs.version }} + steps: + - name: Require main branch + if: github.event_name == 'workflow_dispatch' + run: | + if [ "${{ github.ref_name }}" != "main" ]; then + echo "Releases must be run from the main branch (got '${{ github.ref_name }}')." + exit 1 + fi + + - uses: actions/checkout@v4 + with: + token: ${{ secrets.IDEE_GH_TOKEN }} + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - run: npm ci + + - name: Validate skills + run: npm run validate:skills + + - name: Conventional Changelog Action + id: changelog + uses: TriPSs/conventional-changelog-action@v5 + with: + git-user-name: svc-idee-bot + git-user-email: svc_idee_bot@salesforce.com + github-token: ${{ secrets.IDEE_GH_TOKEN }} + tag-prefix: "" + release-count: "0" + skip-on-empty: ${{ github.event_name == 'push' }} + version-file: "package.json" + output-file: "CHANGELOG.md" + + - name: Create Github Release + id: release + uses: ncipollo/release-action@v1 + if: ${{ steps.changelog.outputs.skipped == 'false' }} + with: + name: "${{ steps.changelog.outputs.version }}" + tag: "${{ steps.changelog.outputs.version }}" + commit: ${{ github.sha }} + body: | + ## Changes in @salesforce/afv-skills + + ${{ steps.changelog.outputs.clean_changelog }} + token: ${{ secrets.IDEE_GH_TOKEN }} + skipIfReleaseExists: true + + - name: Publish to npm + id: publish + if: ${{ steps.changelog.outputs.skipped == 'false' && steps.release.outputs.id != '' }} + run: | + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc + npm publish --access public + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Notify cline-fork + if: ${{ steps.changelog.outputs.skipped == 'false' && steps.release.outputs.id != '' && steps.publish.outcome == 'success' }} + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.IDEE_GH_TOKEN }} + repository: forcedotcom/cline-fork + event-type: skills-released + client-payload: '{"version": "${{ steps.changelog.outputs.version }}"}' diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 0000000..22b478c --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,10 @@ +name: pr-validation + +on: + pull_request: + types: [opened, reopened, edited] + branches: [main] + +jobs: + pr-validation: + uses: salesforcecli/github-workflows/.github/workflows/validatePR.yml@main diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 0000000..0e9f492 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,36 @@ +name: Validate Skills + +on: + pull_request: + paths: + - 'skills/**' + - 'scripts/validate-skills.ts' + - 'package.json' + - 'package-lock.json' + - '.nvmrc' + - '.github/workflows/validate-skills.yml' + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - run: npm ci + + # Run against only changed skills when skills files are touched; fall back + # to full corpus validation when only tooling files changed, so validator + # regressions against existing skills are caught in the same PR. + - name: Validate skills + run: | + if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q '^skills/'; then + npm run validate:skills -- --changed --base=origin/${{ github.base_ref }} + else + npm run validate:skills + fi diff --git a/.nvmrc b/.nvmrc index 12419e9..ffa3aec 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.18.1 \ No newline at end of file +v22.22.1 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 45c65fa..7bb5ce5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,459 @@ { - "name": "afv-library", + "name": "@salesforce/afv-skills", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "afv-library", + "name": "@salesforce/afv-skills", + "version": "1.0.0", + "license": "CC-BY-NC-4.0", "devDependencies": { "@salesforce/webapp-template-app-react-sample-b2e-experimental": "*", - "@salesforce/webapp-template-app-react-sample-b2x-experimental": "*" + "@salesforce/webapp-template-app-react-sample-b2x-experimental": "*", + "tsx": "^4.21.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@salesforce/webapp-template-app-react-sample-b2e-experimental": { @@ -22,6 +468,106 @@ "integrity": "sha512-ry4U36CjJx9h7pv5R5q0tVQCHkEvEYm/4u/5Ysh3UqTz89s3B9o/GPb0JK9fZ2Mgt/DCd0zfPUm1oRlCplnhGQ==", "dev": true, "license": "SEE LICENSE IN LICENSE.txt" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } } } } diff --git a/package.json b/package.json index d0e11ad..9004d60 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,22 @@ { - "name": "afv-library", - "description": "AI prompts and rules library for Agentforce Vibes development", - "private": true, + "name": "@salesforce/afv-skills", + "version": "1.0.0", + "description": "Salesforce skills for Agentforce Vibes", + "license": "CC-BY-NC-4.0", + "files": [ + "skills/" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "devDependencies": { + "tsx": "^4.21.0", "@salesforce/webapp-template-app-react-sample-b2e-experimental": "*", "@salesforce/webapp-template-app-react-sample-b2x-experimental": "*" }, "scripts": { + "validate:skills": "tsx scripts/validate-skills.ts", "sync-react-b2e-sample": "node scripts/sync-react-b2e-sample.js", "sync-react-b2x-sample": "node scripts/sync-react-b2x-sample.js" } diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts new file mode 100644 index 0000000..9087b52 --- /dev/null +++ b/scripts/validate-skills.ts @@ -0,0 +1,368 @@ +#!/usr/bin/env tsx +// Validates the skills/ directory structure and SKILL.md format. +// Exits with code 1 if any violations are found. +// +// Usage: +// npm run validate:skills # validate all skills +// npm run validate:skills -- --changed --base=origin/main # validate only skills changed vs base + +import { execSync } from "child_process" +import fs from "fs" +import path from "path" +import { parseArgs } from "util" + +const SKILLS_DIR = path.join(__dirname, "..", "skills") + +/** Parsed context for a single skill directory, built before content checks run. */ +interface SkillContext { + dirName: string + dirPath: string + content: string + frontmatter: Record | null + body: string +} + +/** Return value of every check function. */ +interface CheckResult { + errors: string[] + /** When true and errors is non-empty, skip remaining checks for this entry. */ + fatal?: boolean + /** Defaults to "error". Warnings are printed but do not cause a non-zero exit code. */ + severity?: "error" | "warning" +} + +/** Collected results for a single skill entry. */ +interface SkillResult { + errors: string[] + warnings: string[] +} + +/** Runs before SKILL.md is read — validates directory layout. */ +interface StructureCheck { + description: string + run(dirName: string, dirPath: string): CheckResult +} + +/** Runs after SKILL.md is read — validates file content. */ +interface ContentCheck { + description: string + run(ctx: SkillContext): CheckResult +} + +/** + * Structure checks run on every entry in `skills/` before SKILL.md is read. + * A fatal error aborts content checks for that entry. + */ +const STRUCTURE_CHECKS: StructureCheck[] = [ + { + description: "Entry must be a directory (no loose files in skills/)", + run(dirName, dirPath) { + if (!fs.statSync(dirPath).isDirectory()) { + return { errors: [`Loose file in skills/: ${dirName} (expected only directories)`], fatal: true } + } + return { errors: [] } + }, + }, + { + description: "Skill directory must contain SKILL.md", + run(dirName, dirPath) { + if (!fs.existsSync(path.join(dirPath, "SKILL.md"))) { + return { errors: [`Missing SKILL.md in skills/${dirName}/`], fatal: true } + } + return { errors: [] } + }, + }, + { + description: "Name must be kebab-case (lowercase letters, digits, and hyphens only)", + run(dirName) { + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(dirName)) { + return { errors: [`skills/${dirName}: name must be kebab-case (only lowercase letters, digits, and hyphens)`] } + } + return { errors: [] } + }, + }, + { + description: "Name must be at most 64 characters", + run(dirName) { + if (dirName.length > 64) { + return { errors: [`skills/${dirName}: name is ${dirName.length} characters (maximum 64)`] } + } + return { errors: [] } + }, + }, + { + description: "Name should use gerund form — first word should end in -ing (e.g. generating-apex-tests)", + run(dirName) { + if (!dirName.split("-")[0].endsWith("ing")) { + return { + errors: [`skills/${dirName}: name should use gerund form (e.g. generating-apex-tests, refactoring-triggers)`], + severity: "warning", + } + } + return { errors: [] } + }, + }, + { + description: "Skills must be exactly one level deep (no nested category directories)", + run(dirName, dirPath) { + const errors: string[] = [] + for (const sub of fs.readdirSync(dirPath)) { + const subPath = path.join(dirPath, sub) + if (fs.statSync(subPath).isDirectory() && fs.existsSync(path.join(subPath, "SKILL.md"))) { + errors.push( + `Nested skill detected: skills/${dirName}/${sub}/SKILL.md — skill directories must be exactly one level deep under skills/` + ) + } + } + return { errors } + }, + }, +] + +/** + * Content checks run only on entries that have a valid SKILL.md. + * A fatal error aborts remaining content checks for that entry. + */ +const CONTENT_CHECKS: ContentCheck[] = [ + { + description: "SKILL.md must have a valid YAML frontmatter block (--- ... ---)", + run({ dirName, frontmatter }) { + if (!frontmatter) { + return { + errors: [`skills/${dirName}/SKILL.md: missing or malformed YAML frontmatter (expected --- ... --- block at top)`], + fatal: true, + } + } + return { errors: [] } + }, + }, + { + description: 'Frontmatter "name" must be present and match the directory name', + run({ dirName, frontmatter }) { + if (!frontmatter) return { errors: [] } + if (!frontmatter.name) { + return { errors: [`skills/${dirName}/SKILL.md: missing "name" field in frontmatter`] } + } + if (frontmatter.name !== dirName) { + return { + errors: [ + `skills/${dirName}/SKILL.md: "name" value ("${frontmatter.name}") does not match directory name ("${dirName}")`, + ], + } + } + return { errors: [] } + }, + }, + { + description: 'Frontmatter "description" must be present and non-empty', + run({ dirName, frontmatter }) { + if (!frontmatter) return { errors: [] } + if (!frontmatter.description?.trim()) { + return { errors: [`skills/${dirName}/SKILL.md: missing or empty "description" field in frontmatter`] } + } + return { errors: [] } + }, + }, + { + description: "SKILL.md must have a non-empty body (instructions after the frontmatter block)", + run({ dirName, body }) { + if (!body.trim()) { + return { errors: [`skills/${dirName}/SKILL.md: body (instructions after frontmatter) is empty`] } + } + return { errors: [] } + }, + }, + { + description: "Description must be at least 20 words to be information-rich", + run({ dirName, frontmatter }) { + if (!frontmatter) return { errors: [] } + const words = frontmatter.description?.trim().split(/\s+/) ?? [] + if (words.length < 20) { + return { + errors: [`skills/${dirName}/SKILL.md: description too short (${words.length} word(s), minimum 20)`], + } + } + return { errors: [] } + }, + }, + { + description: "Description must be at most 1024 characters", + run({ dirName, frontmatter }) { + if (!frontmatter) return { errors: [] } + const len = frontmatter.description?.length ?? 0 + if (len > 1024) { + return { errors: [`skills/${dirName}/SKILL.md: description is ${len} characters (maximum 1024)`] } + } + return { errors: [] } + }, + }, + { + description: 'Description must include trigger/activation language (contain "use")', + run({ dirName, frontmatter }) { + if (!frontmatter) return { errors: [] } + if (!frontmatter.description?.toLowerCase().includes("use")) { + return { + errors: [ + `skills/${dirName}/SKILL.md: description must include trigger context (e.g. "Use this skill when...")`, + ], + } + } + return { errors: [] } + }, + }, + { + description: "Skill body should be under 500 lines for context efficiency", + run({ dirName, body }) { + const lines = body.split("\n").length + if (lines > 500) { + return { + errors: [`skills/${dirName}/SKILL.md: body is ${lines} lines (recommended maximum is 500)`], + severity: "warning", + } + } + return { errors: [] } + }, + }, +] + +/** + * 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. + * Deleted skill directories are intentionally excluded — removing a skill is + * valid and requires no structural validation. + */ +function getChangedSkillDirs(base: string): string[] { + const output = execSync(`git diff --name-only ${base}...HEAD`, { encoding: "utf8" }) + return [ + ...new Set( + output + .split("\n") + .filter((f) => f.startsWith("skills/")) + .map((f) => f.split("/")[1]) + .filter(Boolean) + ), + ].filter((dir) => fs.existsSync(path.join(SKILLS_DIR, dir))) +} + +/** + * Parses a SKILL.md file into its frontmatter fields and body. + * `frontmatter` is `null` if the `--- ... ---` block is missing or malformed. + * Wrapping quotes on frontmatter values are stripped. + */ +function parseSkillMd(content: string): { frontmatter: Record | null; body: string } { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/) + if (!match) return { frontmatter: null, body: content } + + const frontmatter: Record = {} + for (const line of match[1].split(/\r?\n/)) { + const colonIdx = line.indexOf(":") + if (colonIdx === -1) continue + const key = line.slice(0, colonIdx).trim() + const raw = line.slice(colonIdx + 1).trim() + // Strip wrapping single or double quotes to match how consumers read values + frontmatter[key] = raw.replace(/^(['"])([\s\S]*)\1$/, "$2") + } + + return { frontmatter, body: content.slice(match[0].length) } +} + +/** + * Runs all structure and content checks for a single skill directory. + * Returns errors (block CI) and warnings (advisory, printed but exit 0) separately. + */ +function validateSkill(dirName: string, dirPath: string): SkillResult { + if (!fs.existsSync(dirPath)) { + return { errors: [`skills/${dirName}: directory not found`], warnings: [] } + } + + const errors: string[] = [] + const warnings: string[] = [] + + const collectIssues = (result: CheckResult): boolean => { + if (result.errors.length === 0) return false + if (result.severity === "warning") { + warnings.push(...result.errors) + } else { + errors.push(...result.errors) + } + return result.fatal === true && result.severity !== "warning" + } + + for (const check of STRUCTURE_CHECKS) { + if (collectIssues(check.run(dirName, dirPath))) return { errors, warnings } + } + + const content = fs.readFileSync(path.join(dirPath, "SKILL.md"), "utf8") + const { frontmatter, body } = parseSkillMd(content) + const ctx: SkillContext = { dirName, dirPath, content, frontmatter, body } + + for (const check of CONTENT_CHECKS) { + if (collectIssues(check.run(ctx))) return { errors, warnings } + } + + return { errors, warnings } +} + +/** CLI entry point. Parses flags, resolves the list of skills to check, and reports results. */ +function main(): void { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + /** Validate only skill dirs touched in this branch vs the given base ref. */ + changed: { type: "boolean", default: false }, + /** Base ref for --changed (e.g. origin/main). Defaults to origin/HEAD. */ + base: { type: "string", default: "origin/HEAD" }, + }, + }) + + let entries: string[] + + if (values.changed) { + entries = getChangedSkillDirs(values.base!) + if (entries.length === 0) { + console.log("No skill directories changed — nothing to validate.") + return + } + console.log(`Validating ${entries.length} changed skill(s): ${entries.join(", ")}`) + } else { + entries = fs.readdirSync(SKILLS_DIR) + } + + const allErrors: string[] = [] + const allWarnings: string[] = [] + let passed = 0 + + for (const entry of entries) { + const { errors, warnings } = validateSkill(entry, path.join(SKILLS_DIR, entry)) + allErrors.push(...errors) + allWarnings.push(...warnings) + if (errors.length === 0) passed++ + } + + const hasIssues = allErrors.length > 0 || allWarnings.length > 0 + const footer = "Spec: https://agentskills.io/specification · Authoring guide: https://github.com/forcedotcom/afv-library#readme" + + if (allWarnings.length > 0) { + console.warn(`\n${allWarnings.length} warning(s):\n`) + for (const w of allWarnings) { + console.warn(` ⚠ ${w}`) + } + console.warn("") + } + + if (allErrors.length > 0) { + console.error(`\nSkill validation failed with ${allErrors.length} error(s):\n`) + for (const err of allErrors) { + console.error(` ✗ ${err}`) + } + console.error("") + console.error(footer) + console.error("") + process.exit(1) + } else { + console.log(`Skill validation passed: ${passed} of ${entries.length} skill(s) checked.`) + if (hasIssues) console.warn(footer) + } +} + +main() diff --git a/skills/apex-development/apex-class/SKILL.md b/skills/apex-class/SKILL.md similarity index 100% rename from skills/apex-development/apex-class/SKILL.md rename to skills/apex-class/SKILL.md diff --git a/skills/apex-development/apex-class/examples/AccountDeduplicationBatch.cls b/skills/apex-class/examples/AccountDeduplicationBatch.cls similarity index 100% rename from skills/apex-development/apex-class/examples/AccountDeduplicationBatch.cls rename to skills/apex-class/examples/AccountDeduplicationBatch.cls diff --git a/skills/apex-development/apex-class/examples/AccountSelector.cls b/skills/apex-class/examples/AccountSelector.cls similarity index 100% rename from skills/apex-development/apex-class/examples/AccountSelector.cls rename to skills/apex-class/examples/AccountSelector.cls diff --git a/skills/apex-development/apex-class/examples/AccountService.cls b/skills/apex-class/examples/AccountService.cls similarity index 100% rename from skills/apex-development/apex-class/examples/AccountService.cls rename to skills/apex-class/examples/AccountService.cls diff --git a/skills/apex-development/apex-class/templates/abstract.cls b/skills/apex-class/templates/abstract.cls similarity index 100% rename from skills/apex-development/apex-class/templates/abstract.cls rename to skills/apex-class/templates/abstract.cls diff --git a/skills/apex-development/apex-class/templates/batch.cls b/skills/apex-class/templates/batch.cls similarity index 100% rename from skills/apex-development/apex-class/templates/batch.cls rename to skills/apex-class/templates/batch.cls diff --git a/skills/apex-development/apex-class/templates/domain.cls b/skills/apex-class/templates/domain.cls similarity index 100% rename from skills/apex-development/apex-class/templates/domain.cls rename to skills/apex-class/templates/domain.cls diff --git a/skills/apex-development/apex-class/templates/dto.cls b/skills/apex-class/templates/dto.cls similarity index 100% rename from skills/apex-development/apex-class/templates/dto.cls rename to skills/apex-class/templates/dto.cls diff --git a/skills/apex-development/apex-class/templates/exception.cls b/skills/apex-class/templates/exception.cls similarity index 100% rename from skills/apex-development/apex-class/templates/exception.cls rename to skills/apex-class/templates/exception.cls diff --git a/skills/apex-development/apex-class/templates/interface.cls b/skills/apex-class/templates/interface.cls similarity index 100% rename from skills/apex-development/apex-class/templates/interface.cls rename to skills/apex-class/templates/interface.cls diff --git a/skills/apex-development/apex-class/templates/queueable.cls b/skills/apex-class/templates/queueable.cls similarity index 100% rename from skills/apex-development/apex-class/templates/queueable.cls rename to skills/apex-class/templates/queueable.cls diff --git a/skills/apex-development/apex-class/templates/schedulable.cls b/skills/apex-class/templates/schedulable.cls similarity index 100% rename from skills/apex-development/apex-class/templates/schedulable.cls rename to skills/apex-class/templates/schedulable.cls diff --git a/skills/apex-development/apex-class/templates/selector.cls b/skills/apex-class/templates/selector.cls similarity index 100% rename from skills/apex-development/apex-class/templates/selector.cls rename to skills/apex-class/templates/selector.cls diff --git a/skills/apex-development/apex-class/templates/service.cls b/skills/apex-class/templates/service.cls similarity index 100% rename from skills/apex-development/apex-class/templates/service.cls rename to skills/apex-class/templates/service.cls diff --git a/skills/apex-development/apex-class/templates/utility.cls b/skills/apex-class/templates/utility.cls similarity index 100% rename from skills/apex-development/apex-class/templates/utility.cls rename to skills/apex-class/templates/utility.cls diff --git a/skills/testing-automation/apex-test-class/SKILL.md b/skills/apex-test-class/SKILL.md similarity index 100% rename from skills/testing-automation/apex-test-class/SKILL.md rename to skills/apex-test-class/SKILL.md diff --git a/skills/testing-automation/apex-test-class/references/assertion-patterns.md b/skills/apex-test-class/references/assertion-patterns.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/assertion-patterns.md rename to skills/apex-test-class/references/assertion-patterns.md diff --git a/skills/testing-automation/apex-test-class/references/async-testing.md b/skills/apex-test-class/references/async-testing.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/async-testing.md rename to skills/apex-test-class/references/async-testing.md diff --git a/skills/testing-automation/apex-test-class/references/mocking-patterns.md b/skills/apex-test-class/references/mocking-patterns.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/mocking-patterns.md rename to skills/apex-test-class/references/mocking-patterns.md diff --git a/skills/testing-automation/apex-test-class/references/test-data-factory.md b/skills/apex-test-class/references/test-data-factory.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/test-data-factory.md rename to skills/apex-test-class/references/test-data-factory.md diff --git a/skills/metadata-deployments/deployment-readiness-check/SKILL.md b/skills/deployment-readiness-check/SKILL.md similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/SKILL.md rename to skills/deployment-readiness-check/SKILL.md diff --git a/skills/metadata-deployments/deployment-readiness-check/assets/deployment_checklist.md b/skills/deployment-readiness-check/assets/deployment_checklist.md similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/assets/deployment_checklist.md rename to skills/deployment-readiness-check/assets/deployment_checklist.md diff --git a/skills/metadata-deployments/deployment-readiness-check/references/rollback_procedures.md b/skills/deployment-readiness-check/references/rollback_procedures.md similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/references/rollback_procedures.md rename to skills/deployment-readiness-check/references/rollback_procedures.md diff --git a/skills/metadata-deployments/deployment-readiness-check/scripts/check_metadata.sh b/skills/deployment-readiness-check/scripts/check_metadata.sh similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/scripts/check_metadata.sh rename to skills/deployment-readiness-check/scripts/check_metadata.sh diff --git a/skills/apex-development/trigger-refactor-pipeline/SKILL.md b/skills/trigger-refactor-pipeline/SKILL.md similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/SKILL.md rename to skills/trigger-refactor-pipeline/SKILL.md diff --git a/skills/apex-development/trigger-refactor-pipeline/assets/test_template.apex b/skills/trigger-refactor-pipeline/assets/test_template.apex similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/assets/test_template.apex rename to skills/trigger-refactor-pipeline/assets/test_template.apex diff --git a/skills/apex-development/trigger-refactor-pipeline/references/handler_patterns.md b/skills/trigger-refactor-pipeline/references/handler_patterns.md similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/references/handler_patterns.md rename to skills/trigger-refactor-pipeline/references/handler_patterns.md diff --git a/skills/apex-development/trigger-refactor-pipeline/scripts/analyze_trigger.py b/skills/trigger-refactor-pipeline/scripts/analyze_trigger.py similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/scripts/analyze_trigger.py rename to skills/trigger-refactor-pipeline/scripts/analyze_trigger.py