From 0639db7a046e97306e2f6526c69d12638b23c080 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:15:55 -0700 Subject: [PATCH 01/17] chore: add runtime and pnpm workspace foundation (#7488) --- .gitignore | 2 +- .nvmrc | 1 + .sisyphus/boulder.json | 20 + .../skills-repo-framework/learnings.md | 3 + .sisyphus/plans/skills-repo-framework.md | 1098 +++++++++++++++++ LICENSE | 338 +++++ package.json | 15 + pnpm-lock.yaml | 9 + pnpm-workspace.yaml | 9 + skills/placeholder-skill/SKILL.md | 27 + 10 files changed, 1521 insertions(+), 1 deletion(-) create mode 100644 .nvmrc create mode 100644 .sisyphus/boulder.json create mode 100644 .sisyphus/notepads/skills-repo-framework/learnings.md create mode 100644 .sisyphus/plans/skills-repo-framework.md create mode 100644 LICENSE create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 skills/placeholder-skill/SKILL.md diff --git a/.gitignore b/.gitignore index 9adaaeb..a2e397d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ composer.lock .env /node_modules **/node_modules -local-config.json \ No newline at end of file +local-config.json diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..9e2934a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.11.1 diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json new file mode 100644 index 0000000..6dcabb0 --- /dev/null +++ b/.sisyphus/boulder.json @@ -0,0 +1,20 @@ +{ + "active_plan": "/Users/faction23/Projects/gravity/gravityskills/.sisyphus/plans/skills-repo-framework.md", + "started_at": "2026-05-13T13:05:31.787Z", + "session_ids": [ + "ses_1deaab1ceffezTzEEWrUlWMQbr", + "ses_1de8e267affePUc0rgTpYoD5dH" + ], + "plan_name": "skills-repo-framework", + "agent": "atlas", + "task_sessions": { + "todo:1": { + "task_key": "todo:1", + "task_label": "1", + "task_title": "Runtime + pnpm Workspace Foundation", + "session_id": "ses_1de8e267affePUc0rgTpYoD5dH", + "agent": "build", + "updated_at": "2026-05-13T13:07:26.733Z" + } + } +} \ No newline at end of file diff --git a/.sisyphus/notepads/skills-repo-framework/learnings.md b/.sisyphus/notepads/skills-repo-framework/learnings.md new file mode 100644 index 0000000..21405ba --- /dev/null +++ b/.sisyphus/notepads/skills-repo-framework/learnings.md @@ -0,0 +1,3 @@ + +## 2026-05-13 +- Created backlog issue https://github.com/gravityforms/backlog/issues/7488 for the public gravityskills repository framework. Used `orion_feature_request.md` with labels `Enhancement`, `Orion`, and `Gravity`, assigned to `faction23`. diff --git a/.sisyphus/plans/skills-repo-framework.md b/.sisyphus/plans/skills-repo-framework.md new file mode 100644 index 0000000..539d720 --- /dev/null +++ b/.sisyphus/plans/skills-repo-framework.md @@ -0,0 +1,1098 @@ +# Gravity Skills Repo Framework + +## TL;DR + +> **Quick Summary**: Set up the public skills repository framework for Gravity products with three distribution channels — GitHub Releases (zip artifacts), npm CLI installer (`@gravity/skills`), and Claude Code plugin marketplace — using pnpm 11 with full supply chain hardening. +> +> **Deliverables**: +> - pnpm 11 monorepo with supply chain hardening (minimumReleaseAge, blockExoticSubdeps, trustPolicy) +> - `skills/` directory with placeholder skill following Agent Skills spec +> - `packages/skills-cli/` with `@gravity/skills` CLI scaffold (`--help`, `install `) +> - `.claude-plugin/marketplace.json` for native Claude Code plugin install +> - GitHub Actions: CI validation workflow + tag-triggered release workflow + disabled npm publish scaffold +> - Skill validation script (frontmatter, naming, structure) +> - Zip packaging script (deterministic, excludes junk files) +> - GPL-2.0+ LICENSE +> +> **Estimated Effort**: Medium +> **Parallel Execution**: YES - 3 waves +> **Critical Path**: Task 1 → Task 3 → Task 5 → Task 7 → Task 9 + +--- + +## Context + +### Original Request +Set up a public skills repo framework for Gravity products (Gravity Forms, add-ons, Gravity SMTP, etc.). The repo (`gravityforms/gravityskills`) is currently private and empty. It needs to serve skills via zip downloads from GitHub Releases, an npm CLI installer under `@gravity/skills` using trusted publishing only, and Claude Code plugin marketplace integration. No actual publishing happens in this branch due to npm supply chain concerns. + +### Interview Summary +**Key Discussions**: +- **Zip distribution**: CI generates zips on tag, attaches to GitHub Releases (not committed to repo like Vercel) +- **Claude plugin marketplace**: Yes — `.claude-plugin/marketplace.json`, no approval process needed +- **npm package**: CLI installer (`npx @gravity/skills install `), scaffold only, no publish yet +- **Repo structure**: `skills/` + `packages/` (Vercel/Anthropic pattern) +- **pnpm version**: v11 (supply chain hardening on by default) +- **Node**: 24.11.1, npm: 11.6.2 +- **License**: GPL-2.0+ +- **First skills**: Placeholder/test skill only +- **Testing**: Basic CI validation (frontmatter, zip integrity, build) +- **Branch protection**: Not in this branch + +**Research Findings**: +- Vercel ships skill dirs + zip siblings in repo, uses per-skill CI workflows +- Anthropic uses `.claude-plugin/marketplace.json` with plugins grouping skills into installable bundles +- Agent Skills spec: `SKILL.md` with YAML frontmatter (name, description required), optional scripts/references/assets dirs +- npm trusted publishing: OIDC-based, requires npm 11.5.1+, first publish must be manual, then configure OIDC on npmjs.com +- pnpm 11 defaults: `minimumReleaseAge: 1440`, `blockExoticSubdeps: true`, `verifyDepsBeforeRun: "install"`, postinstall disabled + +### Metis Review +**Identified Gaps** (addressed): +- `.agents/` vs `skills/` canonical location: `.agents/` is internal/gitignored, `skills/` is the public directory — no conflict +- Zip structure: each skill zipped as `/SKILL.md` (not flat) +- CLI minimum behavior for this branch: `--help` + `install ` stub that downloads from GitHub Releases +- Release tagging: manual tags only for this branch, no changesets +- Exclude `.DS_Store`, `node_modules`, `.git`, `.env` from zips +- Placeholder skill clearly marked as test-only +- npm trusted publishing workflow disabled by default, with documentation on manual first-publish requirement + +--- + +## Work Objectives + +### Core Objective +Establish the complete framework for a public skills repository with three distribution channels, validated by CI, with no actual publishing. + +### Concrete Deliverables +- Root `package.json` with `packageManager` field, `.nvmrc`, `pnpm-workspace.yaml` +- `LICENSE` (GPL-2.0+) +- `skills/placeholder-skill/SKILL.md` following Agent Skills spec +- `packages/skills-cli/` — minimal TypeScript CLI package (`@gravity/skills`) +- `.claude-plugin/marketplace.json` +- `.github/workflows/ci.yml` — validation on PR/push +- `.github/workflows/release.yml` — zip generation + GitHub Release on tags +- `.github/workflows/npm-publish.yml` — disabled scaffold for future trusted publishing +- `scripts/validate-skills.ts` — frontmatter + structure validation +- `scripts/pack-skills.ts` — deterministic zip generation +- Updated `README.md` with installation instructions for all three channels +- Updated `.gitignore` for build artifacts + +### Definition of Done +- [ ] `pnpm install --frozen-lockfile` exits 0 +- [ ] `pnpm build` exits 0 +- [ ] `pnpm test` exits 0 +- [ ] `pnpm skills:validate` exits 0 +- [ ] `pnpm skills:pack` exits 0 and produces valid zips +- [ ] CLI `--help` works +- [ ] All GitHub Actions workflow files pass `actionlint` + +### Must Have +- pnpm 11 with explicit supply chain config +- Node 24.11.1 pinned in `.nvmrc` +- `packageManager` field in root `package.json` +- GPL-2.0+ LICENSE at root +- At least one skill following Agent Skills spec +- Skill validation (frontmatter required fields, naming convention, structure) +- Zip packaging that excludes junk files +- CI workflow that runs on PR +- Release workflow that generates zips on tag +- Claude plugin marketplace manifest + +### Must NOT Have (Guardrails) +- NO actual npm publishing — workflow exists but is disabled/manual-only +- NO real product skills — placeholder only +- NO migration of `.agents/` content (those are internal, gitignored) +- NO branch protection setup (done manually later) +- NO docs site or catalog website +- NO Changesets, Turborepo, Nx, or complex monorepo tooling +- NO postinstall scripts in any package +- NO long-lived npm tokens or secrets for CI validation +- NO duplicate skill sources between `.agents/` and `skills/` +- NO over-abstracted CLI — minimal install command, not a full SDK + +--- + +## Verification Strategy (MANDATORY) + +> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed. No exceptions. + +### Test Decision +- **Infrastructure exists**: NO (new repo) +- **Automated tests**: YES (tests-after) — validation scripts + unit tests for CLI +- **Framework**: `bun test` (ships with bun, zero config, fast) + +### QA Policy +Every task MUST include agent-executed QA scenarios. +Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`. + +- **CLI**: Use Bash — run command, validate output, check exit code +- **Scripts**: Use Bash — run validation/pack scripts, check outputs +- **CI Workflows**: Use `actionlint` for syntax validation +- **Zips**: Use `unzip -t` and `zipinfo` for integrity/contents + +--- + +## Execution Strategy + +### Pre-Implementation: Issue + Spec Creation + +> This plan requires a GitHub issue and spec BEFORE implementation begins. +> The orchestrator must follow this sequence with user checkpoints. + +**Step A: Create GitHub Issue** +- Use `.agents/github-issue-creator` skill to create issue in `gravityforms/backlog` +- Team: Orion, Label: `gravity`, Assignee: `faction23` +- Type: Feature request +- Title: `Gravity: Public skills repository framework` +- Body: Dense summary of this plan's objectives, deliverables, and three distribution channels +- **PAUSE after creation** — user reviews issue on GitHub before proceeding + +**Step B: Create Spec (after user confirms issue)** +- Use `.agents/skill-creator` or spec skill to create `.specs/skills-repo-framework.md` +- Link spec's `source:` field to the created issue URL +- Spec captures: objectives, architecture decisions, distribution channels, supply chain strategy +- **PAUSE after creation** — user reviews spec before implementation begins + +**Step C: Begin Implementation** +- Create feature branch from `main` +- Execute Wave 1-3 tasks below +- All commits reference the issue number + +### Parallel Execution Waves + +``` +Wave 1 (Start Immediately - foundation): +├── Task 1: Runtime + pnpm workspace foundation [quick] +├── Task 2: License + README scaffold [quick] +├── Task 3: Placeholder skill [quick] + +Wave 2 (After Wave 1 - tooling + packages): +├── Task 4: Skill validation script [unspecified-high] +├── Task 5: Zip packaging script [unspecified-high] +├── Task 6: CLI package scaffold [unspecified-high] +├── Task 7: Claude plugin marketplace manifest [quick] + +Wave 3 (After Wave 2 - CI + release): +├── Task 8: CI validation workflow [unspecified-high] +├── Task 9: Release artifact workflow [unspecified-high] +├── Task 10: npm trusted publishing scaffold [quick] + +Wave FINAL (After ALL tasks): +├── F1: Plan compliance audit (oracle) +├── F2: Code quality review (unspecified-high) +├── F3: Real manual QA (unspecified-high) +├── F4: Scope fidelity check (deep) +-> Present results -> Get explicit user okay +``` + +### Dependency Matrix + +| Task | Depends On | Blocks | +|------|-----------|--------| +| 1 | - | 4, 5, 6, 7, 8, 9, 10 | +| 2 | - | 8 | +| 3 | - | 4, 5, 7, 8 | +| 4 | 1, 3 | 8 | +| 5 | 1, 3 | 8, 9 | +| 6 | 1 | 8, 10 | +| 7 | 1, 3 | 8 | +| 8 | 4, 5, 6, 7 | F1-F4 | +| 9 | 5 | F1-F4 | +| 10 | 6 | F1-F4 | + +### Agent Dispatch Summary + +- **Wave 1**: 3 tasks — T1 `quick`, T2 `quick`, T3 `quick` +- **Wave 2**: 4 tasks — T4 `unspecified-high`, T5 `unspecified-high`, T6 `unspecified-high`, T7 `quick` +- **Wave 3**: 3 tasks — T8 `unspecified-high`, T9 `unspecified-high`, T10 `quick` +- **FINAL**: 4 tasks — F1 `oracle`, F2 `unspecified-high`, F3 `unspecified-high`, F4 `deep` + +--- + +## TODOs + +- [ ] 1. Runtime + pnpm Workspace Foundation + + **What to do**: + - Create `.nvmrc` with `24.11.1` + - Create root `package.json` with: + - `"name": "gravityskills"` (private, not published) + - `"private": true` + - `"packageManager": "pnpm@11.0.8"` (latest stable pnpm 11) + - `"engines": { "node": "24.11.1", "npm": "11.6.2" }` + - Scripts: `build`, `test`, `skills:validate`, `skills:pack` + - Create `pnpm-workspace.yaml` with: + - `packages: ["packages/*"]` + - `minimumReleaseAge: 1440` (1 day — pnpm 11 default, but be explicit) + - `blockExoticSubdeps: true` (pnpm 11 default, be explicit) + - `trustPolicy: no-downgrade` + - `verifyDepsBeforeRun: install` + - Run `pnpm install` to generate lockfile + - Update `.gitignore` to include: `node_modules/`, `dist/`, `.DS_Store`, `*.tgz`, `.sisyphus/` + + **Must NOT do**: + - Do not add Turborepo, Nx, Changesets, or any complex monorepo tooling + - Do not add postinstall scripts + - Do not add unnecessary dependencies + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 2, 3) + - **Blocks**: Tasks 4, 5, 6, 7, 8, 9, 10 + - **Blocked By**: None + + **References**: + + **Pattern References**: + - pnpm 11 supply chain docs: https://pnpm.io/supply-chain-security — minimumReleaseAge, blockExoticSubdeps, trustPolicy settings + - pnpm workspace config: https://pnpm.io/pnpm-workspace_yaml — workspace package glob and settings format + + **API/Type References**: + - `packageManager` field docs: https://nodejs.org/api/packages.html#packagemanager — enables Corepack auto-install + + **External References**: + - pnpm 11.0.8 release: https://github.com/pnpm/pnpm/releases/tag/v11.0.8 — latest stable, fixes self-update and tarball URL issues + + **Acceptance Criteria**: + - [ ] `.nvmrc` contains `24.11.1` + - [ ] `package.json` has `packageManager` field starting with `pnpm@11` + - [ ] `pnpm-workspace.yaml` has explicit `minimumReleaseAge: 1440` + - [ ] `pnpm install --frozen-lockfile` exits 0 + - [ ] `pnpm-lock.yaml` exists and is committed + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: pnpm install succeeds with frozen lockfile + Tool: Bash + Preconditions: Clean checkout, Node 24.11.1 available + Steps: + 1. Run `cat .nvmrc` → expect output `24.11.1` + 2. Run `node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).packageManager)"` → expect starts with `pnpm@11` + 3. Run `pnpm install --frozen-lockfile` → expect exit 0 + 4. Run `grep minimumReleaseAge pnpm-workspace.yaml` → expect `1440` + 5. Run `grep blockExoticSubdeps pnpm-workspace.yaml` → expect `true` + Expected Result: All commands exit 0 with correct values + Failure Indicators: pnpm install fails, missing config fields + Evidence: .sisyphus/evidence/task-1-pnpm-foundation.txt + + Scenario: .gitignore excludes build artifacts and internal dirs + Tool: Bash + Preconditions: .gitignore exists + Steps: + 1. Run `grep -c 'node_modules' .gitignore` → expect ≥ 1 + 2. Run `grep -c 'dist/' .gitignore` → expect ≥ 1 + 3. Run `grep -c '.DS_Store' .gitignore` → expect ≥ 1 + 4. Run `grep -c '.sisyphus/' .gitignore` → expect ≥ 1 + Expected Result: All patterns present including .sisyphus/ + Failure Indicators: Missing patterns in .gitignore + Evidence: .sisyphus/evidence/task-1-gitignore.txt + ``` + + **Commit**: YES + - Message: `chore: add runtime and pnpm workspace foundation` + - Files: `.nvmrc`, `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `.gitignore` + +- [ ] 2. License + README Scaffold + + **What to do**: + - Add `LICENSE` file with GPL-2.0+ full text + - Update `README.md` with: + - Project description (skills for Gravity products) + - Three installation methods: Claude Code plugin (`/plugin marketplace add`), CLI (`npx @gravity/skills install`), direct download (GitHub Releases) + - Contributing section (brief, point to Agent Skills spec) + - License badge + - Note that npm publishing is not yet active + + **Must NOT do**: + - Do not create a docs site + - Do not add CONTRIBUTING.md or CODE_OF_CONDUCT.md yet + - Do not mention internal tooling or `.agents/` + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 3) + - **Blocks**: Task 8 + - **Blocked By**: None + + **References**: + + **External References**: + - GPL-2.0 license text: https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + - Agent Skills spec: https://agentskills.io/specification — link for contributors + - Anthropic README example: https://github.com/anthropics/skills/blob/main/README.md — installation section pattern + + **Acceptance Criteria**: + - [ ] `LICENSE` file exists with GPL-2.0 text + - [ ] `README.md` mentions all three installation methods + - [ ] `README.md` does not reference `.agents/` or internal tooling + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: License file is valid GPL-2.0 + Tool: Bash + Preconditions: LICENSE exists + Steps: + 1. Run `head -1 LICENSE` → expect contains "GNU GENERAL PUBLIC LICENSE" + 2. Run `grep -c 'Version 2' LICENSE` → expect ≥ 1 + Expected Result: GPL-2.0 identified + Failure Indicators: Wrong license text or missing file + Evidence: .sisyphus/evidence/task-2-license.txt + + Scenario: README has all three install methods + Tool: Bash + Preconditions: README.md exists + Steps: + 1. Run `grep -c 'plugin marketplace add' README.md` → expect ≥ 1 + 2. Run `grep -c '@gravity/skills' README.md` → expect ≥ 1 + 3. Run `grep -c 'Releases' README.md` → expect ≥ 1 + 4. Run `grep -c '.agents' README.md` → expect 0 + Expected Result: All install methods documented, no internal references + Failure Indicators: Missing install method or internal leakage + Evidence: .sisyphus/evidence/task-2-readme.txt + ``` + + **Commit**: YES + - Message: `chore: add GPL-2.0+ license and README scaffold` + - Files: `LICENSE`, `README.md` + +- [ ] 3. Placeholder Skill + + **What to do**: + - Create `skills/placeholder-skill/SKILL.md` following Agent Skills spec: + ```yaml + --- + name: placeholder-skill + description: A placeholder skill for testing the Gravity Skills repository framework. Not intended for production use. + license: GPL-2.0+ + metadata: + author: gravityforms + version: "0.0.1" + --- + ``` + - Add markdown body with brief instructions explaining this is a test/placeholder + - Optionally add a `references/` dir with a sample reference file to validate zip packaging + + **Must NOT do**: + - Do not create any real Gravity product skills + - Do not include any proprietary Gravity code or documentation + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 2) + - **Blocks**: Tasks 4, 5, 7 + - **Blocked By**: None + + **References**: + + **Pattern References**: + - Agent Skills spec: https://agentskills.io/specification — SKILL.md frontmatter format, required fields, naming rules + - Anthropic template: https://github.com/anthropics/skills/blob/main/template/SKILL.md — minimal skill template + - Vercel skill example: `skills/deploy-to-vercel/SKILL.md` in `vercel-labs/agent-skills` — structure with references dir + + **Acceptance Criteria**: + - [ ] `skills/placeholder-skill/SKILL.md` exists + - [ ] Frontmatter has required `name` and `description` fields + - [ ] `name` matches parent directory name (`placeholder-skill`) + - [ ] `name` is lowercase, hyphens only, no consecutive hyphens + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: Placeholder skill follows Agent Skills spec + Tool: Bash + Preconditions: skills/placeholder-skill/SKILL.md exists + Steps: + 1. Run `test -f skills/placeholder-skill/SKILL.md` → expect exit 0 + 2. Run `head -2 skills/placeholder-skill/SKILL.md` → expect first line is `---` + 3. Run `grep 'name: placeholder-skill' skills/placeholder-skill/SKILL.md` → expect match + 4. Run `grep 'description:' skills/placeholder-skill/SKILL.md` → expect match + Expected Result: Valid SKILL.md with required frontmatter + Failure Indicators: Missing file, missing frontmatter, name mismatch + Evidence: .sisyphus/evidence/task-3-placeholder-skill.txt + + Scenario: Skill name matches directory name + Tool: Bash + Preconditions: Skill directory exists + Steps: + 1. Extract name from SKILL.md frontmatter + 2. Compare with parent directory name `placeholder-skill` + Expected Result: Names match exactly + Evidence: .sisyphus/evidence/task-3-name-match.txt + ``` + + **Commit**: YES + - Message: `feat: add placeholder skill following Agent Skills spec` + - Files: `skills/placeholder-skill/SKILL.md`, optionally `skills/placeholder-skill/references/` + +- [ ] 4. Skill Validation Script + + **What to do**: + - Create `scripts/validate-skills.ts` that: + - Scans `skills/*/SKILL.md` for all skills + - Parses YAML frontmatter (use `yaml` package or manual parsing) + - Validates required fields: `name`, `description` + - Validates `name` format: lowercase, hyphens only, 1-64 chars, no consecutive hyphens, no leading/trailing hyphens + - Validates `name` matches parent directory name + - Checks for duplicate skill names + - Checks `description` is 1-1024 characters + - Reports errors with file paths and exits non-zero on failure + - Add a test file `scripts/__tests__/validate-skills.test.ts` with: + - Test for valid skill passing + - Test for missing `name` field + - Test for invalid `name` format (uppercase, spaces, consecutive hyphens) + - Test for name/directory mismatch + - Test for duplicate names + - Wire up as `pnpm skills:validate` in root package.json + + **Must NOT do**: + - Do not add heavy validation frameworks (no ajv, zod, etc.) + - Do not validate optional fields beyond basic type checks + - Do not fetch or validate external references + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 5, 6, 7) + - **Blocks**: Task 8 + - **Blocked By**: Tasks 1, 3 + + **References**: + + **Pattern References**: + - Agent Skills spec naming rules: https://agentskills.io/specification — `name` field constraints (lowercase, hyphens, 1-64 chars, must match parent dir) + - `skills/placeholder-skill/SKILL.md` — the skill to validate against (created in Task 3) + + **External References**: + - YAML frontmatter parsing: standard `---` delimited block at top of file, parse with simple regex or `yaml` npm package + + **WHY Each Reference Matters**: + - The spec defines exact validation rules — don't invent constraints, follow the spec precisely + - The placeholder skill is the test fixture — validation must pass on it + + **Acceptance Criteria**: + - [ ] `scripts/validate-skills.ts` exists + - [ ] `pnpm skills:validate` exits 0 with placeholder skill present + - [ ] Validation catches: missing name, invalid name format, name/dir mismatch, missing description + - [ ] `bun test scripts/__tests__/validate-skills.test.ts` passes + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: Validation passes for valid placeholder skill + Tool: Bash + Preconditions: placeholder-skill exists with valid SKILL.md + Steps: + 1. Run `pnpm skills:validate` + 2. Check exit code → expect 0 + 3. Check output contains "placeholder-skill" as validated + Expected Result: Clean validation pass + Failure Indicators: Non-zero exit, error messages + Evidence: .sisyphus/evidence/task-4-validate-pass.txt + + Scenario: Validation fails for invalid skill + Tool: Bash + Preconditions: Create temp invalid skill with uppercase name + Steps: + 1. Create `skills/Invalid-Skill/SKILL.md` with `name: Invalid-Skill` + 2. Run `pnpm skills:validate` + 3. Check exit code → expect non-zero + 4. Check output mentions invalid name format + 5. Remove temp skill + Expected Result: Validation catches the invalid name + Failure Indicators: Validation passes when it shouldn't + Evidence: .sisyphus/evidence/task-4-validate-fail.txt + ``` + + **Commit**: YES + - Message: `feat: add skill validation script` + - Files: `scripts/validate-skills.ts`, `scripts/__tests__/validate-skills.test.ts`, `package.json` (script entry) + +- [ ] 5. Zip Packaging Script + + **What to do**: + - Create `scripts/pack-skills.ts` that: + - Scans `skills/*/` for all skill directories + - Creates a `dist/skills/` output directory + - For each skill, creates `.zip` containing the skill directory (not flat — zip root is the skill dir name) + - Excludes: `node_modules/`, `.git/`, `.DS_Store`, `.env`, `*.tgz`, `__pycache__/` + - Uses Node's built-in `child_process` to call `zip` or uses `archiver` npm package + - Logs each zip created with size + - Exits non-zero if any zip fails + - Add test file `scripts/__tests__/pack-skills.test.ts`: + - Test zip is created for placeholder skill + - Test zip contains SKILL.md + - Test zip excludes .DS_Store + - Test zip root is skill directory name (not flat) + - Wire up as `pnpm skills:pack` in root package.json + + **Must NOT do**: + - Do not commit zips to the repo (CI generates them) + - Do not create one monolithic zip of all skills + - Do not include any files from outside the skill directory + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 4, 6, 7) + - **Blocks**: Tasks 8, 9 + - **Blocked By**: Tasks 1, 3 + + **References**: + + **Pattern References**: + - Vercel zip pattern: `vercel-labs/agent-skills` has `Archive.zip` inside each skill dir + `.zip` at skills/ root — we do NOT follow this (we generate to `dist/`) + + **External References**: + - `archiver` npm package: https://www.npmjs.com/package/archiver — if avoiding shell `zip` dependency + - Alternative: Node `child_process.execSync('zip ...')` — simpler if `zip` is available in CI (ubuntu-latest has it) + + **Acceptance Criteria**: + - [ ] `scripts/pack-skills.ts` exists + - [ ] `pnpm skills:pack` exits 0 + - [ ] `dist/skills/placeholder-skill.zip` is created + - [ ] `unzip -t dist/skills/placeholder-skill.zip` exits 0 + - [ ] `zipinfo -1 dist/skills/placeholder-skill.zip` includes `placeholder-skill/SKILL.md` + - [ ] `zipinfo -1 dist/skills/placeholder-skill.zip` does NOT include `.DS_Store` or `node_modules` + - [ ] `bun test scripts/__tests__/pack-skills.test.ts` passes + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: Zip packaging creates valid archive + Tool: Bash + Preconditions: placeholder-skill exists, dist/ is clean + Steps: + 1. Run `rm -rf dist/` + 2. Run `pnpm skills:pack` → expect exit 0 + 3. Run `test -f dist/skills/placeholder-skill.zip` → expect exit 0 + 4. Run `unzip -t dist/skills/placeholder-skill.zip` → expect exit 0 + 5. Run `zipinfo -1 dist/skills/placeholder-skill.zip` → expect contains `placeholder-skill/SKILL.md` + Expected Result: Valid zip with correct structure + Failure Indicators: Missing zip, corrupt archive, wrong structure + Evidence: .sisyphus/evidence/task-5-pack-valid.txt + + Scenario: Zip excludes junk files + Tool: Bash + Preconditions: Create .DS_Store in skill dir + Steps: + 1. Run `touch skills/placeholder-skill/.DS_Store` + 2. Run `pnpm skills:pack` + 3. Run `zipinfo -1 dist/skills/placeholder-skill.zip | grep -c '.DS_Store'` → expect 0 + 4. Run `rm skills/placeholder-skill/.DS_Store` + Expected Result: Junk files excluded from zip + Failure Indicators: .DS_Store appears in zip contents + Evidence: .sisyphus/evidence/task-5-pack-excludes.txt + ``` + + **Commit**: YES + - Message: `feat: add skill zip packaging script` + - Files: `scripts/pack-skills.ts`, `scripts/__tests__/pack-skills.test.ts`, `package.json` (script entry) + +- [ ] 6. @gravity/skills CLI Package Scaffold + + **What to do**: + - Create `packages/skills-cli/` with: + - `package.json`: + - `"name": "@gravity/skills"` + - `"version": "0.0.1"` (not published yet) + - `"bin": { "gravity-skills": "./dist/index.js" }` + - `"type": "module"` + - `"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" }` + - `"license": "GPL-2.0+"` + - `"repository": { "type": "git", "url": "https://github.com/gravityforms/gravityskills" }` + - `"engines": { "node": ">=20" }` (broader than repo requirement for CLI users) + - Scripts: `build`, `test` + - `tsconfig.json` for TypeScript compilation + - `src/index.ts` — entry point with CLI argument parsing: + - `--help` / `-h`: Show usage (list skills, install skill) + - `--version` / `-v`: Show package version + - `install `: Download skill zip from GitHub Releases and extract to target dir + - `list`: List available skills (reads from a bundled or fetched manifest) + - `src/commands/install.ts` — download zip from `https://github.com/gravityforms/gravityskills/releases/latest/download/.zip`, extract to current dir or `--target` path + - `src/commands/list.ts` — fetch/display available skills + - Minimal deps: **ZERO npm dependencies**. Use only Node built-ins: + - `https` / `fetch` (Node 24 has global fetch) for downloading zips from GitHub Releases + - `fs` / `fs/promises` for file operations + - `path` for path manipulation + - `child_process` for `unzip` or use `zlib` + manual zip parsing + - `crypto` for integrity checks if needed + - `readline` for minimal interactive prompts if needed + - Parse YAML frontmatter with simple regex (3 lines: split on `---`, no yaml lib needed for reading metadata) + - The published package must have `"dependencies": {}` in package.json — absolutely no npm deps + - Build with bundler (rolldown, esbuild, or tsc) so the dist is a single self-contained file + - This makes the CLI resilient to npm supply chain attacks — nothing to compromise except our own code + - Test file `src/__tests__/cli.test.ts`: + - Test `--help` output + - Test `--version` output + - Test `install` with mock/stub (don't actually download) + - Run `pnpm install` from root to wire up workspace + + **Must NOT do**: + - Do not add heavy CLI frameworks (commander, yargs, oclif) + - Do not add ANY npm dependencies — the published package must have zero deps + - Do not implement authentication or private skill access + - Do not add SDK/programmatic API + - Do not publish to npm + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 4, 5, 7) + - **Blocks**: Tasks 8, 10 + - **Blocked By**: Task 1 + + **References**: + + **Pattern References**: + - npm `bin` field: `package.json` `bin` maps CLI command name to entry file + - GitHub Releases download URL pattern: `https://github.com/{owner}/{repo}/releases/latest/download/{filename}` + + **External References**: + - npm trusted publishing: https://docs.npmjs.com/trusted-publishers/ — `publishConfig` setup for future OIDC publishing + - Node built-in `https` for downloads, `child_process` for `unzip` + + **WHY Each Reference Matters**: + - `publishConfig` must be correct now so the trusted publishing workflow works when enabled + - Download URL pattern is how the CLI fetches skills from GitHub Releases + + **Acceptance Criteria**: + - [ ] `packages/skills-cli/package.json` has `"name": "@gravity/skills"` + - [ ] `pnpm --filter @gravity/skills build` exits 0 + - [ ] `node packages/skills-cli/dist/index.js --help` exits 0 and shows usage + - [ ] `node packages/skills-cli/dist/index.js --version` shows `0.0.1` + - [ ] `node -e "const p=JSON.parse(require('fs').readFileSync('packages/skills-cli/package.json','utf8')); console.log(Object.keys(p.dependencies||{}).length)"` outputs `0` (zero dependencies) + - [ ] `bun test packages/skills-cli/src/__tests__/cli.test.ts` passes + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: CLI --help shows install command + Tool: Bash + Preconditions: CLI is built + Steps: + 1. Run `pnpm --filter @gravity/skills build` → expect exit 0 + 2. Run `node packages/skills-cli/dist/index.js --help` → expect exit 0 + 3. Check output contains "install" → expect match + 4. Check output contains "list" → expect match + Expected Result: Help shows available commands + Failure Indicators: Build fails, help missing commands + Evidence: .sisyphus/evidence/task-6-cli-help.txt + + Scenario: CLI --version shows correct version + Tool: Bash + Preconditions: CLI is built + Steps: + 1. Run `node packages/skills-cli/dist/index.js --version` + 2. Check output → expect `0.0.1` + Expected Result: Version matches package.json + Evidence: .sisyphus/evidence/task-6-cli-version.txt + ``` + + **Commit**: YES + - Message: `feat: scaffold @gravity/skills CLI package` + - Files: `packages/skills-cli/*`, `pnpm-lock.yaml` + +- [ ] 7. Claude Plugin Marketplace Manifest + + **What to do**: + - Create `.claude-plugin/marketplace.json` with: + ```json + { + "name": "gravity-skills", + "owner": { + "name": "Gravity Forms", + "email": "support@gravityforms.com" + }, + "metadata": { + "description": "Official skills for Gravity Forms, Gravity SMTP, and other Gravity products", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "gravity-skills", + "description": "Skills for working with Gravity products", + "source": "./", + "strict": false, + "skills": [ + "./skills/placeholder-skill" + ] + } + ] + } + ``` + - Validate JSON is well-formed + + **Must NOT do**: + - Do not add real product skills to the plugins array yet + - Do not reference `.agents/` skills + - Do not invent skill paths that don't exist + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 4, 5, 6) + - **Blocks**: Task 8 + - **Blocked By**: Tasks 1, 3 + + **References**: + + **Pattern References**: + - Anthropic marketplace.json: `anthropics/skills/.claude-plugin/marketplace.json` — exact schema to follow with `name`, `owner`, `metadata`, `plugins` array + + **Acceptance Criteria**: + - [ ] `.claude-plugin/marketplace.json` exists + - [ ] JSON is valid (`node -e "JSON.parse(require('fs').readFileSync('.claude-plugin/marketplace.json','utf8'))"` exits 0) + - [ ] `plugins[0].skills` references only existing skill directories + - [ ] `name` field is `"gravity-skills"` + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: Marketplace manifest is valid JSON with correct structure + Tool: Bash + Preconditions: .claude-plugin/marketplace.json exists + Steps: + 1. Run `node -e "const m=JSON.parse(require('fs').readFileSync('.claude-plugin/marketplace.json','utf8')); console.log(m.name)"` → expect `gravity-skills` + 2. Run `node -e "const m=JSON.parse(require('fs').readFileSync('.claude-plugin/marketplace.json','utf8')); console.log(m.plugins[0].skills[0])"` → expect `./skills/placeholder-skill` + 3. Run `test -d skills/placeholder-skill` → expect exit 0 + Expected Result: Valid manifest pointing to existing skills + Failure Indicators: Invalid JSON, missing skills, broken references + Evidence: .sisyphus/evidence/task-7-marketplace.txt + + Scenario: Referenced skill paths exist + Tool: Bash + Preconditions: Manifest exists + Steps: + 1. Parse all skill paths from plugins[].skills[] + 2. For each path, verify directory exists + Expected Result: All referenced paths exist + Evidence: .sisyphus/evidence/task-7-skill-refs.txt + ``` + + **Commit**: YES + - Message: `feat: add Claude plugin marketplace manifest` + - Files: `.claude-plugin/marketplace.json` + +- [ ] 8. CI Validation Workflow + + **What to do**: + - Create `.github/workflows/ci.yml`: + - Trigger: push to `main`, pull_request to `main` + - Jobs: + - `validate`: + - `runs-on: ubuntu-latest` + - Setup pnpm 11 via `pnpm/action-setup@v4` + - Setup Node 24.11.1 via `actions/setup-node@v4` with pnpm cache + - `pnpm install --frozen-lockfile` + - `pnpm build` + - `pnpm test` + - `pnpm skills:validate` + - `pnpm skills:pack` (verify zips can be created) + - Verify zips with `unzip -t dist/skills/*.zip` + + **Must NOT do**: + - Do not require any secrets for CI validation + - Do not publish anything + - Do not add complex matrix builds + - Do not add caching beyond pnpm's built-in + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 3 + - **Blocks**: F1-F4 + - **Blocked By**: Tasks 4, 5, 6, 7 + + **References**: + + **Pattern References**: + - Vercel CI workflow: `.github/workflows/react-best-practices-ci.yml` in `vercel-labs/agent-skills` — pnpm/action-setup + setup-node + validate + build pattern + - pnpm/action-setup@v4: https://github.com/pnpm/action-setup — official pnpm GitHub Action + + **External References**: + - actions/setup-node@v4 with pnpm cache: set `cache: 'pnpm'` and `cache-dependency-path: 'pnpm-lock.yaml'` + + **Acceptance Criteria**: + - [ ] `.github/workflows/ci.yml` exists + - [ ] Workflow triggers on push to main and PR to main + - [ ] Workflow runs: install, build, test, skills:validate, skills:pack + - [ ] No secrets required for validation jobs + - [ ] `actionlint .github/workflows/ci.yml` exits 0 (if actionlint available) + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: CI workflow YAML is valid + Tool: Bash + Preconditions: .github/workflows/ci.yml exists + Steps: + 1. Run `node -e "require('yaml').parse(require('fs').readFileSync('.github/workflows/ci.yml','utf8'))"` or `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"` → expect exit 0 + 2. Verify `on.push.branches` includes `main` + 3. Verify `on.pull_request.branches` includes `main` + 4. Verify steps include `pnpm install`, `pnpm build`, `pnpm test`, `pnpm skills:validate`, `pnpm skills:pack` + Expected Result: Valid workflow with all required steps + Failure Indicators: Invalid YAML, missing triggers, missing steps + Evidence: .sisyphus/evidence/task-8-ci-workflow.txt + + Scenario: No secrets referenced in CI workflow + Tool: Bash + Preconditions: ci.yml exists + Steps: + 1. Run `grep -c 'secrets\.' .github/workflows/ci.yml` → expect 0 + Expected Result: No secrets used + Evidence: .sisyphus/evidence/task-8-no-secrets.txt + ``` + + **Commit**: YES + - Message: `ci: add validation workflow` + - Files: `.github/workflows/ci.yml` + +- [ ] 9. Release Artifact Workflow + + **What to do**: + - Create `.github/workflows/release.yml`: + - Trigger: push tags matching `v*` (e.g. `v0.1.0`) + - Permissions: `contents: write` (to create releases) + - Jobs: + - `release`: + - `runs-on: ubuntu-latest` + - Setup pnpm + Node (same as CI) + - `pnpm install --frozen-lockfile` + - `pnpm build` + - `pnpm test` + - `pnpm skills:validate` + - `pnpm skills:pack` + - Create GitHub Release using `softprops/action-gh-release@v2` or `gh release create`: + - Upload all `dist/skills/*.zip` as release assets + - Generate release notes from tag + - Mark as latest + + **Must NOT do**: + - Do not publish to npm (separate workflow) + - Do not auto-create tags + - Do not create draft releases (make them actual releases) + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES (with Task 10) + - **Parallel Group**: Wave 3 + - **Blocks**: F1-F4 + - **Blocked By**: Task 5 + + **References**: + + **External References**: + - `softprops/action-gh-release@v2`: https://github.com/softprops/action-gh-release — upload release assets with glob pattern + - Alternative: `gh release create $TAG dist/skills/*.zip --generate-notes` — simpler, uses GitHub CLI + + **Acceptance Criteria**: + - [ ] `.github/workflows/release.yml` exists + - [ ] Triggers only on `v*` tags + - [ ] Has `contents: write` permission + - [ ] Uploads `dist/skills/*.zip` as release assets + - [ ] Runs validation before creating release (install, build, test, validate, pack) + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: Release workflow triggers on version tags only + Tool: Bash + Preconditions: release.yml exists + Steps: + 1. Parse YAML and check `on.push.tags` → expect includes `v*` + 2. Verify no `on.push.branches` trigger + 3. Verify `permissions.contents` is `write` + Expected Result: Tag-only trigger with write permissions + Failure Indicators: Branch triggers, missing permissions + Evidence: .sisyphus/evidence/task-9-release-trigger.txt + + Scenario: Release workflow validates before creating release + Tool: Bash + Preconditions: release.yml exists + Steps: + 1. Check step order: install before build before test before validate before pack before release + 2. Verify release step references `dist/skills/*.zip` + Expected Result: Validation gates release creation + Evidence: .sisyphus/evidence/task-9-release-order.txt + ``` + + **Commit**: YES + - Message: `ci: add release artifact workflow` + - Files: `.github/workflows/release.yml` + +- [ ] 10. npm Trusted Publishing Scaffold + + **What to do**: + - Create `.github/workflows/npm-publish.yml`: + - Trigger: `workflow_dispatch` only (manual, disabled by default) + - Comment block at top explaining: + 1. First publish must be manual (`npm login && cd packages/skills-cli && npm publish --access public`) + 2. Then configure trusted publisher on npmjs.com (org: gravityforms, repo: gravityskills, workflow: npm-publish.yml) + 3. Then this workflow can be enabled + - Permissions: `id-token: write`, `contents: read` + - Jobs: + - `publish`: + - `runs-on: ubuntu-latest` + - Setup pnpm + Node 24.x (must be 24.x for npm 11.5.1+) + - `pnpm install --frozen-lockfile` + - `pnpm --filter @gravity/skills build` + - `pnpm --filter @gravity/skills test` + - `cd packages/skills-cli && npm publish --access public` + - `registry-url: 'https://registry.npmjs.org'` in setup-node (REQUIRED for trusted publishing) + + **Must NOT do**: + - Do not enable auto-trigger (no push/tag triggers) + - Do not store npm tokens + - Do not actually publish + - Do not add `--provenance` flag (automatic with trusted publishing) + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: [] + + **Parallelization**: + - **Can Run In Parallel**: YES (with Tasks 8, 9) + - **Parallel Group**: Wave 3 + - **Blocks**: F1-F4 + - **Blocked By**: Task 6 + + **References**: + + **External References**: + - npm trusted publishing docs: https://docs.npmjs.com/trusted-publishers/ — OIDC setup, first-publish requirement + - Key gotcha: first publish must be manual, trusted publishing only works from second release onward + - Key gotcha: Node 24.x required for npm 11.5.1+ (trusted publishing requirement) + - Key gotcha: `registry-url` must be explicitly set in `actions/setup-node` even though it's the default + - Key gotcha: provenance attestations are automatic with trusted publishing from public repos — no `--provenance` flag needed + + **Acceptance Criteria**: + - [ ] `.github/workflows/npm-publish.yml` exists + - [ ] Only triggers on `workflow_dispatch` (no push/tag/PR triggers) + - [ ] Has `id-token: write` and `contents: read` permissions + - [ ] Has `registry-url: 'https://registry.npmjs.org'` in setup-node + - [ ] Has comment block explaining manual first-publish requirement + - [ ] Does NOT reference any npm tokens or secrets + + **QA Scenarios (MANDATORY)**: + + ``` + Scenario: npm publish workflow is manual-only + Tool: Bash + Preconditions: npm-publish.yml exists + Steps: + 1. Run `grep -c 'workflow_dispatch' .github/workflows/npm-publish.yml` → expect ≥ 1 + 2. Run `grep -c 'push:' .github/workflows/npm-publish.yml` → expect 0 + 3. Run `grep -c 'pull_request:' .github/workflows/npm-publish.yml` → expect 0 + 4. Run `grep 'id-token' .github/workflows/npm-publish.yml` → expect match containing `write` + 5. Run `grep 'registry-url' .github/workflows/npm-publish.yml` → expect match + 6. Run `grep -c 'NPM_TOKEN\|NODE_AUTH_TOKEN.*secrets' .github/workflows/npm-publish.yml` → expect 0 + Expected Result: Manual-only workflow with OIDC, no tokens + Failure Indicators: Auto-triggers present, missing OIDC, token references + Evidence: .sisyphus/evidence/task-10-npm-scaffold.txt + + Scenario: First-publish documentation exists in workflow + Tool: Bash + Preconditions: npm-publish.yml exists + Steps: + 1. Run `grep -c 'first publish' .github/workflows/npm-publish.yml` → expect ≥ 1 (case insensitive) + 2. Run `grep -c 'npm login' .github/workflows/npm-publish.yml` → expect ≥ 1 + Expected Result: Manual first-publish instructions present + Evidence: .sisyphus/evidence/task-10-npm-docs.txt + ``` + + **Commit**: YES + - Message: `ci: add disabled npm trusted publishing scaffold` + - Files: `.github/workflows/npm-publish.yml` + +--- + +## Final Verification Wave (MANDATORY — after ALL implementation tasks) + +> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing. + +- [ ] F1. **Plan Compliance Audit** — `oracle` + Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. + Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT` + +- [ ] F2. **Code Quality Review** — `unspecified-high` + Run `tsc --noEmit` + linter + `bun test`. Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names. + Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT` + +- [ ] F3. **Real Manual QA** — `unspecified-high` + Start from clean state. Run `pnpm install`, `pnpm build`, `pnpm test`, `pnpm skills:validate`, `pnpm skills:pack`. Test CLI `--help` and `install placeholder-skill`. Verify zip contents. Test each distribution channel locally. + Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT` + +- [ ] F4. **Scope Fidelity Check** — `deep` + For each task: read "What to do", read actual diff. Verify 1:1 — everything in spec was built, nothing beyond spec was built. Check "Must NOT do" compliance. Detect cross-task contamination. Flag unaccounted changes. + Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT` + +--- + +## Commit Strategy + +| Commit | Message | Files | +|--------|---------|-------| +| 1 | `chore: add runtime and pnpm workspace foundation` | `.nvmrc`, `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `.gitignore` | +| 2 | `chore: add GPL-2.0+ license and README scaffold` | `LICENSE`, `README.md` | +| 3 | `feat: add placeholder skill following Agent Skills spec` | `skills/placeholder-skill/SKILL.md` | +| 4 | `feat: add skill validation script` | `scripts/validate-skills.ts`, tests | +| 5 | `feat: add skill zip packaging script` | `scripts/pack-skills.ts`, tests | +| 6 | `feat: scaffold @gravity/skills CLI package` | `packages/skills-cli/*` | +| 7 | `feat: add Claude plugin marketplace manifest` | `.claude-plugin/marketplace.json` | +| 8 | `ci: add validation workflow` | `.github/workflows/ci.yml` | +| 9 | `ci: add release artifact workflow` | `.github/workflows/release.yml` | +| 10 | `ci: add disabled npm trusted publishing scaffold` | `.github/workflows/npm-publish.yml` | + +--- + +## Success Criteria + +### Verification Commands +```bash +pnpm install --frozen-lockfile # Expected: exits 0 +pnpm build # Expected: exits 0 +pnpm test # Expected: exits 0, all tests pass +pnpm skills:validate # Expected: exits 0, placeholder skill valid +pnpm skills:pack # Expected: exits 0, creates dist/skills/placeholder-skill.zip +unzip -t dist/skills/placeholder-skill.zip # Expected: exits 0 +node packages/skills-cli/dist/index.js --help # Expected: shows help with install command +actionlint # Expected: exits 0 on all workflow files +``` + +### Final Checklist +- [ ] All "Must Have" present +- [ ] All "Must NOT Have" absent +- [ ] All tests pass +- [ ] All three distribution channels scaffolded (GitHub Releases, npm CLI, Claude plugin) +- [ ] Supply chain hardening configured and documented diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9efa6fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/package.json b/package.json new file mode 100644 index 0000000..f538254 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "gravityskills", + "private": true, + "packageManager": "pnpm@11.0.8", + "engines": { + "node": "24.11.1", + "npm": "11.6.2" + }, + "scripts": { + "build": "pnpm -r build", + "test": "pnpm -r test", + "skills:validate": "node --import tsx/esm scripts/validate-skills.ts", + "skills:pack": "node --import tsx/esm scripts/pack-skills.ts" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..9b60ae1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,9 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..869fbc7 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +packages: + - "packages/*" + +# Supply chain hardening (pnpm 11) +# https://pnpm.io/supply-chain-security +minimumReleaseAge: 1440 # 1 day — newly published packages not resolved for 24h +blockExoticSubdeps: true # Block git/tarball transitive dependencies +trustPolicy: no-downgrade # Reject packages with reduced trust level vs prior releases +verifyDepsBeforeRun: install # Verify lockfile integrity before any pnpm run diff --git a/skills/placeholder-skill/SKILL.md b/skills/placeholder-skill/SKILL.md new file mode 100644 index 0000000..6bb610d --- /dev/null +++ b/skills/placeholder-skill/SKILL.md @@ -0,0 +1,27 @@ +--- +name: placeholder-skill +description: A placeholder skill for testing the Gravity Skills repository framework. Not intended for production use. Use this to verify the skill validation, zip packaging, and distribution pipeline works correctly. +license: GPL-2.0+ +compatibility: Designed for Claude Code and compatible AI coding agents +metadata: + author: gravityforms + version: "0.0.1" +--- + +# Placeholder Skill + +> **This is a test skill.** It exists to validate the Gravity Skills repository framework and distribution pipeline. It does not provide any real functionality. + +This skill is used to verify: +- Skill validation (`pnpm skills:validate`) correctly parses SKILL.md frontmatter +- Zip packaging (`pnpm skills:pack`) correctly bundles skill directories +- GitHub Releases distribution works end-to-end +- Claude Code plugin marketplace installation works + +## When to use + +Do not use this skill for real work. It is a framework validation fixture only. + +## References + +- [Framework validation notes](references/README.md) From 7587acfc409dd5b2be790dc7e8505189e55b1d4b Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:17:00 -0700 Subject: [PATCH 02/17] feat: add placeholder skill following Agent Skills spec (#7488) --- skills/placeholder-skill/references/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 skills/placeholder-skill/references/README.md diff --git a/skills/placeholder-skill/references/README.md b/skills/placeholder-skill/references/README.md new file mode 100644 index 0000000..3a2e7e7 --- /dev/null +++ b/skills/placeholder-skill/references/README.md @@ -0,0 +1,5 @@ +# Placeholder Skill — References + +This directory exists to validate that the zip packaging script correctly includes subdirectories. + +If you are reading this inside a zip file, the packaging pipeline is working correctly. From 21604a219670116d9d4828c13ff3f240dbcb816a Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:20:00 -0700 Subject: [PATCH 03/17] feat: add placeholder skill following Agent Skills spec (#7488) --- .gitignore | 10 ++++++---- .sisyphus/boulder.json | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index a2e397d..daa0fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -.svn/ -.agents/ -.specs/ +/.svn/ +/.agents/ +/.specs/ tmp/ -.idea/ +/.idea/ codeception.yml composer.json composer.lock @@ -12,3 +12,5 @@ composer.lock /node_modules **/node_modules local-config.json +dist/ +.sisyphus/ diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json index 6dcabb0..36ce867 100644 --- a/.sisyphus/boulder.json +++ b/.sisyphus/boulder.json @@ -3,7 +3,9 @@ "started_at": "2026-05-13T13:05:31.787Z", "session_ids": [ "ses_1deaab1ceffezTzEEWrUlWMQbr", - "ses_1de8e267affePUc0rgTpYoD5dH" + "ses_1de8e267affePUc0rgTpYoD5dH", + "ses_1de85de77ffeX8NI6g4Y61hj2w", + "ses_1de8615a0ffeN54729IBEdtQwG" ], "plan_name": "skills-repo-framework", "agent": "atlas", @@ -12,9 +14,10 @@ "task_key": "todo:1", "task_label": "1", "task_title": "Runtime + pnpm Workspace Foundation", - "session_id": "ses_1de8e267affePUc0rgTpYoD5dH", - "agent": "build", - "updated_at": "2026-05-13T13:07:26.733Z" + "session_id": "ses_1de8615a0ffeN54729IBEdtQwG", + "agent": "Sisyphus-Junior", + "category": "quick", + "updated_at": "2026-05-13T13:18:56.475Z" } } } \ No newline at end of file From c6c1194b7be5fd6a179797410201a6b7cb26c99f Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:31:04 -0700 Subject: [PATCH 04/17] feat: add skill zip packaging script (#7488) --- package.json | 5 + pnpm-lock.yaml | 373 +++++++++++++++++++++++++- pnpm-workspace.yaml | 12 +- scripts/__tests__/pack-skills.test.ts | 53 ++++ scripts/pack-skills.ts | 92 +++++++ 5 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 scripts/__tests__/pack-skills.test.ts create mode 100644 scripts/pack-skills.ts diff --git a/package.json b/package.json index f538254..0b3e4ea 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,10 @@ "test": "pnpm -r test", "skills:validate": "node --import tsx/esm scripts/validate-skills.ts", "skills:pack": "node --import tsx/esm scripts/pack-skills.ts" + }, + "devDependencies": { + "@types/bun": "^1.3.13", + "@types/node": "^25.7.0", + "tsx": "^4.21.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b60ae1..1f07182 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,4 +6,375 @@ settings: importers: - .: {} + .: + devDependencies: + '@types/bun': + specifier: ^1.3.13 + version: 1.3.13 + '@types/node': + specifier: ^25.7.0 + version: 25.7.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + + packages/skills-cli: + devDependencies: + '@types/bun': + specifier: ^1.3.3 + version: 1.3.13 + '@types/node': + specifier: ^24.10.1 + version: 24.12.4 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/bun@1.3.13': + resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==} + + '@types/node@24.12.4': + resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + + '@types/node@25.7.0': + resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} + + bun-types@1.3.13: + resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici-types@7.21.0: + resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@types/bun@1.3.13': + dependencies: + bun-types: 1.3.13 + + '@types/node@24.12.4': + dependencies: + undici-types: 7.16.0 + + '@types/node@25.7.0': + dependencies: + undici-types: 7.21.0 + + bun-types@1.3.13: + dependencies: + '@types/node': 25.7.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + resolve-pkg-maps@1.0.0: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + undici-types@7.21.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 869fbc7..56732fc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,11 @@ packages: # Supply chain hardening (pnpm 11) # https://pnpm.io/supply-chain-security -minimumReleaseAge: 1440 # 1 day — newly published packages not resolved for 24h -blockExoticSubdeps: true # Block git/tarball transitive dependencies -trustPolicy: no-downgrade # Reject packages with reduced trust level vs prior releases -verifyDepsBeforeRun: install # Verify lockfile integrity before any pnpm run +minimumReleaseAge: 1440 # 1 day — newly published packages not resolved for 24h +blockExoticSubdeps: true # Block git/tarball transitive dependencies +trustPolicy: no-downgrade # Reject packages with reduced trust level vs prior releases +verifyDepsBeforeRun: install # Verify lockfile integrity before any pnpm run +allowBuilds: + esbuild: false +ignoredBuiltDependencies: + - esbuild diff --git a/scripts/__tests__/pack-skills.test.ts b/scripts/__tests__/pack-skills.test.ts new file mode 100644 index 0000000..771c8e5 --- /dev/null +++ b/scripts/__tests__/pack-skills.test.ts @@ -0,0 +1,53 @@ +/// + +import { execFileSync } from 'node:child_process'; +import { existsSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; + +const repoRoot = join(import.meta.dirname, '..', '..'); +const distDir = join(repoRoot, 'dist'); +const zipPath = join(distDir, 'skills', 'placeholder-skill.zip'); + +function zipEntries(): string[] { + return execFileSync('zipinfo', ['-1', zipPath], { + cwd: repoRoot, + encoding: 'utf8', + }) + .trim() + .split('\n') + .filter(Boolean); +} + +describe('pack skills', () => { + beforeAll(() => { + rmSync(distDir, { recursive: true, force: true }); + execFileSync('pnpm', ['skills:pack'], { cwd: repoRoot, stdio: 'pipe' }); + }); + + afterAll(() => { + rmSync(distDir, { recursive: true, force: true }); + }); + + test('creates the placeholder skill zip', () => { + expect(existsSync(zipPath)).toBe(true); + }); + + test('contains SKILL.md under the skill directory root', () => { + expect(zipEntries()).toContain('placeholder-skill/SKILL.md'); + }); + + test('keeps the skill directory as the zip root', () => { + const entries = zipEntries(); + + expect(entries).not.toContain('SKILL.md'); + expect(entries.every((entry) => entry.startsWith('placeholder-skill/'))).toBe(true); + }); + + test('excludes ignored files and directories', () => { + const entries = zipEntries(); + + expect(entries.some((entry) => entry.includes('.DS_Store'))).toBe(false); + expect(entries.some((entry) => entry.includes('node_modules/'))).toBe(false); + }); +}); diff --git a/scripts/pack-skills.ts b/scripts/pack-skills.ts new file mode 100644 index 0000000..91afa2c --- /dev/null +++ b/scripts/pack-skills.ts @@ -0,0 +1,92 @@ +{ +const { execFileSync } = require('node:child_process'); +const { mkdirSync, readdirSync, rmSync, statSync } = require('node:fs'); +const { join, relative } = require('node:path'); + +const repoRoot = process.cwd(); +const skillsDir = join(repoRoot, 'skills'); +const outputDir = join(repoRoot, 'dist', 'skills'); + +const excludedNames = new Set(['node_modules', '.git', '.DS_Store', '.env', '__pycache__']); + +function getSkillDirectories(): string[] { + return readdirSync(skillsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); +} + +function shouldExclude(path: string): boolean { + const parts = path.split('/'); + const name = parts[parts.length - 1] ?? ''; + + return parts.some((part) => excludedNames.has(part)) || name.endsWith('.tgz'); +} + +function getIncludedFiles(skillName: string): string[] { + const skillDir = join(skillsDir, skillName); + const files: string[] = []; + + function walk(directory: string): void { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const absolutePath = join(directory, entry.name); + const archivePath = relative(skillsDir, absolutePath).split('/').join('/'); + + if (shouldExclude(archivePath)) { + continue; + } + + if (entry.isDirectory()) { + walk(absolutePath); + } else if (entry.isFile()) { + files.push(archivePath); + } + } + } + + walk(skillDir); + + return files.sort(); +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) { + return `${bytes} B`; + } + + return `${(bytes / 1024).toFixed(1)} KB`; +} + +function packSkills(): void { + mkdirSync(outputDir, { recursive: true }); + + const skills = getSkillDirectories(); + + for (const skillName of skills) { + const zipPath = join(outputDir, `${skillName}.zip`); + const includedFiles = getIncludedFiles(skillName); + + rmSync(zipPath, { force: true }); + + if (includedFiles.length === 0) { + throw new Error(`No files found to package for ${skillName}`); + } + + execFileSync('zip', ['-q', '-r', zipPath, ...includedFiles], { + cwd: skillsDir, + stdio: 'pipe', + }); + + const size = statSync(zipPath).size; + console.log(`Created ${relative(repoRoot, zipPath)} (${formatBytes(size)})`); + } +} + +try { + packSkills(); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to pack skills: ${message}`); + process.exitCode = 1; +} +} From ec05e9c0990d1953ff976ff7c661fe28994b6f59 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:32:20 -0700 Subject: [PATCH 05/17] feat: add skill validation script (#7488) --- scripts/__tests__/validate-skills.test.ts | 94 ++++++++++ scripts/validate-skills.ts | 212 ++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 scripts/__tests__/validate-skills.test.ts create mode 100644 scripts/validate-skills.ts diff --git a/scripts/__tests__/validate-skills.test.ts b/scripts/__tests__/validate-skills.test.ts new file mode 100644 index 0000000..916fb4f --- /dev/null +++ b/scripts/__tests__/validate-skills.test.ts @@ -0,0 +1,94 @@ +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +// @ts-ignore Bun provides this module at test runtime. +import { afterEach, describe, expect, test } from 'bun:test'; + +const { validateSkills } = require('../validate-skills'); + +const tempRoots: string[] = []; + +function createRepo(): string { + const repoRoot = mkdtempSync(join(tmpdir(), 'gravityskills-validate-')); + tempRoots.push(repoRoot); + mkdirSync(join(repoRoot, 'skills'), { recursive: true }); + + return repoRoot; +} + +function writeSkill(repoRoot: string, directoryName: string, frontmatter: string): void { + const skillDir = join(repoRoot, 'skills', directoryName); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), `---\n${frontmatter}---\n\n# ${directoryName}\n`); +} + +afterEach(() => { + for (const root of tempRoots.splice(0)) { + rmSync(root, { recursive: true, force: true }); + } +}); + +describe('validateSkills', () => { + test('passes for valid skills', () => { + const repoRoot = createRepo(); + writeSkill(repoRoot, 'placeholder-skill', 'name: placeholder-skill\ndescription: A placeholder skill\n'); + + expect(validateSkills(repoRoot)).toEqual({ + errors: [], + skillCount: 1, + }); + }); + + test('catches missing name', () => { + const repoRoot = createRepo(); + writeSkill(repoRoot, 'missing-name', 'description: A valid description\n'); + + expect(validateSkills(repoRoot).errors).toContain('skills/missing-name/SKILL.md: frontmatter.name is required'); + }); + + test('catches invalid name format', () => { + const repoRoot = createRepo(); + writeSkill(repoRoot, 'invalid-format', 'name: Invalid--Name\ndescription: A valid description\n'); + + expect(validateSkills(repoRoot).errors).toContain( + 'skills/invalid-format/SKILL.md: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens', + ); + }); + + test('catches name and directory mismatch', () => { + const repoRoot = createRepo(); + writeSkill(repoRoot, 'directory-name', 'name: frontmatter-name\ndescription: A valid description\n'); + + expect(validateSkills(repoRoot).errors).toContain( + 'skills/directory-name/SKILL.md: frontmatter.name must match parent directory name "directory-name"', + ); + }); + + test('catches missing description', () => { + const repoRoot = createRepo(); + writeSkill(repoRoot, 'missing-description', 'name: missing-description\n'); + + expect(validateSkills(repoRoot).errors).toContain( + 'skills/missing-description/SKILL.md: frontmatter.description is required', + ); + }); + + test('catches overlong description', () => { + const repoRoot = createRepo(); + writeSkill(repoRoot, 'long-description', `name: long-description\ndescription: ${'a'.repeat(1025)}\n`); + + expect(validateSkills(repoRoot).errors).toContain( + 'skills/long-description/SKILL.md: frontmatter.description must be 1-1024 characters', + ); + }); + + test('catches duplicate names', () => { + const repoRoot = createRepo(); + writeSkill(repoRoot, 'duplicate-one', 'name: duplicate-skill\ndescription: A valid description\n'); + writeSkill(repoRoot, 'duplicate-two', 'name: duplicate-skill\ndescription: Another valid description\n'); + + expect(validateSkills(repoRoot).errors).toContain( + 'skills/duplicate-one/SKILL.md, skills/duplicate-two/SKILL.md: duplicate skill name "duplicate-skill"', + ); + }); +}); diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts new file mode 100644 index 0000000..58a80f9 --- /dev/null +++ b/scripts/validate-skills.ts @@ -0,0 +1,212 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +interface SkillFrontmatter { + name?: unknown; + description?: unknown; + [key: string]: unknown; +} + +interface ValidationResult { + errors: string[]; + skillCount: number; +} + +const skillNamePattern = /^(?!.*--)[a-z](?:[a-z-]{0,62}[a-z])?$/; +const defaultRepoRoot = process.cwd(); + +function formatPath(filePath: string, repoRoot: string): string { + const relativePath = path.relative(repoRoot, filePath); + + return relativePath === '' ? filePath : relativePath.split('\\').join('/'); +} + +function parseScalar(value: string): unknown { + const trimmed = value.trim(); + + if (trimmed === '') { + return ''; + } + + const quote = trimmed[0]; + if ((quote === '"' || quote === "'") && trimmed.endsWith(quote)) { + return trimmed.slice(1, -1); + } + + if (trimmed === 'true') { + return true; + } + + if (trimmed === 'false') { + return false; + } + + return trimmed; +} + +function parseFrontmatter(markdown: string): SkillFrontmatter { + const normalized = markdown.replace(/^\uFEFF/, ''); + + if (!normalized.startsWith('---\n') && normalized.trim() !== '---') { + throw new Error('missing YAML frontmatter opening delimiter'); + } + + const closingDelimiter = normalized.indexOf('\n---', 4); + if (closingDelimiter === -1) { + throw new Error('missing YAML frontmatter closing delimiter'); + } + + const frontmatter = normalized.slice(4, closingDelimiter); + const result: SkillFrontmatter = {}; + const parents: Array> = [result]; + const indents = [0]; + + for (const rawLine of frontmatter.split('\n')) { + if (rawLine.trim() === '' || rawLine.trimStart().startsWith('#')) { + continue; + } + + const match = rawLine.match(/^(\s*)([^:#][^:]*):(?:\s*(.*))?$/); + if (!match) { + continue; + } + + const indent = match[1].length; + const key = match[2].trim(); + const value = match[3] ?? ''; + + while (indents.length > 1 && indent <= indents[indents.length - 1]) { + parents.pop(); + indents.pop(); + } + + const parent = parents[parents.length - 1]; + if (value.trim() === '') { + const child: Record = {}; + parent[key] = child; + parents.push(child); + indents.push(indent); + continue; + } + + parent[key] = parseScalar(value); + } + + return result; +} + +function validateName(filePath: string, repoRoot: string, directoryName: string, name: unknown): string[] { + const location = formatPath(filePath, repoRoot); + const errors: string[] = []; + + if (typeof name !== 'string' || name.trim() === '') { + errors.push(`${location}: frontmatter.name is required`); + return errors; + } + + if (!skillNamePattern.test(name)) { + errors.push(`${location}: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens`); + } + + if (name !== directoryName) { + errors.push(`${location}: frontmatter.name must match parent directory name "${directoryName}"`); + } + + return errors; +} + +function validateDescription(filePath: string, repoRoot: string, description: unknown): string[] { + const location = formatPath(filePath, repoRoot); + + if (typeof description !== 'string' || description.trim() === '') { + return [`${location}: frontmatter.description is required`]; + } + + if (description.length > 1024) { + return [`${location}: frontmatter.description must be 1-1024 characters`]; + } + + return []; +} + +function validateSkills(repoRoot = defaultRepoRoot): ValidationResult { + const skillsDir = path.join(repoRoot, 'skills'); + const errors: string[] = []; + const names = new Map(); + + if (!fs.existsSync(skillsDir)) { + return { + errors: [`${formatPath(skillsDir, repoRoot)}: skills directory does not exist`], + skillCount: 0, + }; + } + + const skillDirectories = fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + + for (const directoryName of skillDirectories) { + const filePath = path.join(skillsDir, directoryName, 'SKILL.md'); + const location = formatPath(filePath, repoRoot); + + if (!fs.existsSync(filePath)) { + errors.push(`${location}: SKILL.md is required`); + continue; + } + + let frontmatter: SkillFrontmatter; + try { + frontmatter = parseFrontmatter(fs.readFileSync(filePath, 'utf8')); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + errors.push(`${location}: ${message}`); + continue; + } + + errors.push(...validateName(filePath, repoRoot, directoryName, frontmatter.name)); + errors.push(...validateDescription(filePath, repoRoot, frontmatter.description)); + + if (typeof frontmatter.name === 'string' && frontmatter.name.trim() !== '') { + const paths = names.get(frontmatter.name) ?? []; + paths.push(location); + names.set(frontmatter.name, paths); + } + } + + for (const [name, paths] of names) { + if (paths.length > 1) { + errors.push(`${paths.join(', ')}: duplicate skill name "${name}"`); + } + } + + return { + errors, + skillCount: skillDirectories.length, + }; +} + +function runValidation(repoRoot = defaultRepoRoot): number { + const result = validateSkills(repoRoot); + + if (result.errors.length > 0) { + console.error(`Skill validation failed with ${result.errors.length} error(s):`); + for (const error of result.errors) { + console.error(`- ${error}`); + } + return 1; + } + + console.log(`Validated ${result.skillCount} skill${result.skillCount === 1 ? '' : 's'}.`); + return 0; +} + +if (path.basename(process.argv[1] ?? '') === 'validate-skills.ts') { + process.exitCode = runValidation(); +} + +module.exports = { + parseFrontmatter, + runValidation, + validateSkills, +}; From c567af2767539c417967a6bf84353787b24a8fa7 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:33:06 -0700 Subject: [PATCH 06/17] feat: scaffold @gravity/skills CLI package (#7488) --- .../skills-repo-framework/learnings.md | 39 +++++++- packages/skills-cli/package.json | 36 +++++++ packages/skills-cli/src/__tests__/cli.test.ts | 59 +++++++++++ packages/skills-cli/src/commands/install.ts | 98 +++++++++++++++++++ packages/skills-cli/src/commands/list.ts | 4 + packages/skills-cli/src/index.ts | 54 ++++++++++ packages/skills-cli/tsconfig.json | 19 ++++ 7 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 packages/skills-cli/package.json create mode 100644 packages/skills-cli/src/__tests__/cli.test.ts create mode 100644 packages/skills-cli/src/commands/install.ts create mode 100644 packages/skills-cli/src/commands/list.ts create mode 100644 packages/skills-cli/src/index.ts create mode 100644 packages/skills-cli/tsconfig.json diff --git a/.sisyphus/notepads/skills-repo-framework/learnings.md b/.sisyphus/notepads/skills-repo-framework/learnings.md index 21405ba..0d85d39 100644 --- a/.sisyphus/notepads/skills-repo-framework/learnings.md +++ b/.sisyphus/notepads/skills-repo-framework/learnings.md @@ -1,3 +1,38 @@ +# Learnings -## 2026-05-13 -- Created backlog issue https://github.com/gravityforms/backlog/issues/7488 for the public gravityskills repository framework. Used `orion_feature_request.md` with labels `Enhancement`, `Orion`, and `Gravity`, assigned to `faction23`. +## [Wave 1] Foundation + +- `packageManager: pnpm@11.0.8` in root package.json — Corepack auto-install +- `pnpm-workspace.yaml` has supply chain hardening: minimumReleaseAge: 1440, blockExoticSubdeps: true, trustPolicy: no-downgrade, verifyDepsBeforeRun: install +- `skills:validate` script: `node --import tsx/esm scripts/validate-skills.ts` +- `skills:pack` script: `node --import tsx/esm scripts/pack-skills.ts` +- `build` and `test` scripts use `pnpm -r` (recursive workspace) +- `skills/placeholder-skill/SKILL.md` exists with valid frontmatter (name: placeholder-skill, description, license, metadata) +- `skills/placeholder-skill/references/README.md` exists for zip packaging validation +- Node 24.11.1 pinned in `.nvmrc` +- GPL-2.0+ LICENSE at root +- README.md is minimal (52 bytes) — was NOT updated with 3 install methods yet (T2 commit may be missing from log) +- Git log shows 5 commits; T2 (license+README) commit message not visible — LICENSE file IS present though +- `packages/` directory does NOT exist yet (CLI scaffold coming in T6) +- `.claude-plugin/` directory does NOT exist yet (T7) +- `scripts/` directory does NOT exist yet (T4, T5) +- `.github/workflows/` does NOT exist yet (T8, T9, T10) +- `tsx` is needed as devDep for running TypeScript scripts via `node --import tsx/esm` + +## Key Constraints +- ZERO npm dependencies in `packages/skills-cli/` published package +- No Turborepo, Nx, Changesets +- No postinstall scripts +- No actual npm publishing +- No real product skills — placeholder only +- bun test for testing +- All commits reference (#7488) + +## Task 5 pack skills - 2026-05-13 +- Root skills:pack script runs through tsx/esm and Node 24 when implemented as CommonJS-style TypeScript, matching existing validate-skills.ts. +- Packaging from skills/ as cwd preserves the skill directory as zip root while preventing files outside the skill directory from being included. + +## Task 6 - @gravity/skills CLI scaffold +- Added packages/skills-cli as @gravity/skills with explicit empty dependencies and dev-only TypeScript/Bun types. +- CLI uses manual argv parsing and Node global fetch; no commander/yargs/oclif. +- pnpm-workspace allowBuilds must set esbuild: false so verifyDepsBeforeRun does not fail on ignored transitive tsx build scripts. diff --git a/packages/skills-cli/package.json b/packages/skills-cli/package.json new file mode 100644 index 0000000..798f2ae --- /dev/null +++ b/packages/skills-cli/package.json @@ -0,0 +1,36 @@ +{ + "name": "@gravity/skills", + "version": "0.0.1", + "description": "Zero-dependency CLI for installing Gravity skills.", + "bin": { + "gravity-skills": "./dist/index.js" + }, + "type": "module", + "main": "./dist/index.js", + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "license": "GPL-2.0+", + "repository": { + "type": "git", + "url": "https://github.com/gravityforms/gravityskills" + }, + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsc", + "test": "bun test" + }, + "dependencies": {}, + "devDependencies": { + "@types/bun": "^1.3.3", + "@types/node": "^24.10.1", + "typescript": "^5.9.3" + } +} diff --git a/packages/skills-cli/src/__tests__/cli.test.ts b/packages/skills-cli/src/__tests__/cli.test.ts new file mode 100644 index 0000000..a90c72c --- /dev/null +++ b/packages/skills-cli/src/__tests__/cli.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test"; +import { run, usage } from "../index"; +import { installSkill } from "../commands/install"; + +function captureConsole(method: "log" | "error", callback: () => Promise | number): Promise<{ code: number; output: string[] }> { + const original = console[method]; + const output: string[] = []; + console[method] = (...args: unknown[]) => { + output.push(args.map(String).join(" ")); + }; + + return Promise.resolve(callback()).then((code) => { + console[method] = original; + return { code, output }; + }, (error) => { + console[method] = original; + throw error; + }); +} + +describe("gravity-skills CLI", () => { + test("--help output contains install and list", async () => { + const { code, output } = await captureConsole("log", () => run(["--help"])); + + expect(code).toBe(0); + expect(output.join("\n")).toContain("install"); + expect(output.join("\n")).toContain("list"); + expect(usage()).toContain("Usage"); + }); + + test("--version outputs 0.0.1", async () => { + const { code, output } = await captureConsole("log", () => run(["--version"])); + + expect(code).toBe(0); + expect(output.join("\n").trim()).toBe("0.0.1"); + }); + + test("install downloads and extracts a skill", async () => { + const calls: string[] = []; + const fakeFetch = async (url: string) => { + calls.push(String(url)); + return new Response(new Uint8Array([80, 75, 3, 4]), { status: 200 }); + }; + + const code = await installSkill(["example-skill", "--target", "/tmp/gravity-skills-test"], { + fetchImpl: fakeFetch, + unzip: (zipPath, targetDir) => { + expect(zipPath).toContain("example-skill"); + expect(targetDir).toBe("/tmp/gravity-skills-test"); + }, + log: () => undefined, + }); + + expect(code).toBe(0); + expect(calls).toEqual([ + "https://github.com/gravityforms/gravityskills/releases/latest/download/example-skill.zip", + ]); + }); +}); diff --git a/packages/skills-cli/src/commands/install.ts b/packages/skills-cli/src/commands/install.ts new file mode 100644 index 0000000..339066f --- /dev/null +++ b/packages/skills-cli/src/commands/install.ts @@ -0,0 +1,98 @@ +import { execSync } from "node:child_process"; +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, join, resolve } from "node:path"; + +export interface InstallOptions { + fetchImpl?: (input: string) => Promise; + unzip?: (zipPath: string, targetDir: string) => void; + log?: (message: string) => void; +} + +interface ParsedInstallArgs { + skillName: string; + targetDir: string; +} + +function parseInstallArgs(args: string[]): ParsedInstallArgs | null { + const [skillName, ...rest] = args; + + if (!skillName || skillName === "--help" || skillName === "-h") { + return null; + } + + let targetDir = process.cwd(); + + for (let index = 0; index < rest.length; index += 1) { + const arg = rest[index]; + + if (arg === "--target") { + const value = rest[index + 1]; + if (!value) { + throw new Error("Missing value for --target"); + } + targetDir = value; + index += 1; + continue; + } + + throw new Error(`Unknown install option: ${arg}`); + } + + return { skillName, targetDir: resolve(targetDir) }; +} + +function installUsage(): string { + return "Usage: gravity-skills install [--target ]"; +} + +function assertSkillName(skillName: string): void { + if (!/^[a-zA-Z0-9._-]+$/.test(skillName) || basename(skillName) !== skillName) { + throw new Error("Skill name may only contain letters, numbers, dots, underscores, and hyphens"); + } +} + +function defaultUnzip(zipPath: string, targetDir: string): void { + execSync(`unzip -oq ${JSON.stringify(zipPath)} -d ${JSON.stringify(targetDir)}`, { + stdio: "inherit", + }); +} + +export async function installSkill(args: string[], options: InstallOptions = {}): Promise { + const parsed = parseInstallArgs(args); + const log = options.log ?? console.log; + + if (!parsed) { + console.log(installUsage()); + return 0; + } + + const { skillName, targetDir } = parsed; + assertSkillName(skillName); + + const fetchImpl = options.fetchImpl ?? fetch; + const unzip = options.unzip ?? defaultUnzip; + const downloadUrl = `https://github.com/gravityforms/gravityskills/releases/latest/download/${skillName}.zip`; + const zipPath = join(tmpdir(), `gravity-skills-${skillName}-${Date.now()}.zip`); + + log(`Downloading ${skillName} from ${downloadUrl}`); + + const response = await fetchImpl(downloadUrl); + if (!response.ok) { + throw new Error(`Download failed with status ${response.status}: ${response.statusText}`); + } + + const bytes = new Uint8Array(await response.arrayBuffer()); + await writeFile(zipPath, bytes); + + try { + await mkdir(targetDir, { recursive: true }); + log(`Extracting ${skillName} to ${targetDir}`); + unzip(zipPath, targetDir); + log(`Installed ${skillName}`); + } finally { + await rm(zipPath, { force: true }); + } + + return 0; +} diff --git a/packages/skills-cli/src/commands/list.ts b/packages/skills-cli/src/commands/list.ts new file mode 100644 index 0000000..48cc637 --- /dev/null +++ b/packages/skills-cli/src/commands/list.ts @@ -0,0 +1,4 @@ +export function listSkills(): number { + console.log("Visit https://github.com/gravityforms/gravityskills/releases for available skills"); + return 0; +} diff --git a/packages/skills-cli/src/index.ts b/packages/skills-cli/src/index.ts new file mode 100644 index 0000000..96f6988 --- /dev/null +++ b/packages/skills-cli/src/index.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import { installSkill } from "./commands/install.js"; +import { listSkills } from "./commands/list.js"; + +const VERSION = "0.0.1"; + +export function usage(): string { + return `Usage: gravity-skills [options] + +Commands: + install [--target ] Download and install a skill from GitHub Releases + list Show where to find available skills + +Options: + -h, --help Show this help message + -v, --version Print the CLI version +`; +} + +export async function run(argv: string[] = process.argv.slice(2)): Promise { + const [command, ...args] = argv; + + if (!command || command === "--help" || command === "-h") { + console.log(usage()); + return 0; + } + + if (command === "--version" || command === "-v") { + console.log(VERSION); + return 0; + } + + if (command === "install") { + return installSkill(args); + } + + if (command === "list") { + return listSkills(); + } + + console.error(`Unknown command: ${command}`); + console.error(usage()); + return 1; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + run().then((code) => { + process.exitCode = code; + }).catch((error: unknown) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + }); +} diff --git a/packages/skills-cli/tsconfig.json b/packages/skills-cli/tsconfig.json new file mode 100644 index 0000000..175ca7e --- /dev/null +++ b/packages/skills-cli/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["node", "bun"], + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/__tests__/**/*.ts"] +} From 64daf0ba2d03d519f1fabf0df774ffd3010c9875 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:36:35 -0700 Subject: [PATCH 07/17] security: raise minimumReleaseAge to 7 days (Mini Shai-Hulud CVE-2026-45321) (#7488) --- pnpm-workspace.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 56732fc..4fda5bc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,7 @@ packages: # Supply chain hardening (pnpm 11) # https://pnpm.io/supply-chain-security -minimumReleaseAge: 1440 # 1 day — newly published packages not resolved for 24h +minimumReleaseAge: 10080 # 7 days — raised from 1d due to Mini Shai-Hulud supply chain attack (May 2026) blockExoticSubdeps: true # Block git/tarball transitive dependencies trustPolicy: no-downgrade # Reject packages with reduced trust level vs prior releases verifyDepsBeforeRun: install # Verify lockfile integrity before any pnpm run From 2674cd55560d4c971677b7dfd86cb82bc98eed7d Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:57:31 -0700 Subject: [PATCH 08/17] feat: add Claude plugin marketplace manifest (#7488) --- .claude-plugin/marketplace.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .claude-plugin/marketplace.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..1ec3f0d --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,22 @@ +{ + "name": "gravity-skills", + "owner": { + "name": "Gravity Forms", + "email": "support@gravityforms.com" + }, + "metadata": { + "description": "Official skills for Gravity Forms, Gravity SMTP, and other Gravity products", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "gravity-skills", + "description": "Skills for working with Gravity products", + "source": "./", + "strict": false, + "skills": [ + "./skills/placeholder-skill" + ] + } + ] +} From 1a290b3591406d9888fee61fad6fcc905d509dab Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 06:57:51 -0700 Subject: [PATCH 09/17] docs: add marketplace manifest notepads (marketplace-manifest) --- .sisyphus/notepads/marketplace-manifest/decisions.md | 1 + .sisyphus/notepads/marketplace-manifest/issues.md | 1 + .sisyphus/notepads/marketplace-manifest/learnings.md | 1 + .sisyphus/notepads/marketplace-manifest/problems.md | 1 + 4 files changed, 4 insertions(+) create mode 100644 .sisyphus/notepads/marketplace-manifest/decisions.md create mode 100644 .sisyphus/notepads/marketplace-manifest/issues.md create mode 100644 .sisyphus/notepads/marketplace-manifest/learnings.md create mode 100644 .sisyphus/notepads/marketplace-manifest/problems.md diff --git a/.sisyphus/notepads/marketplace-manifest/decisions.md b/.sisyphus/notepads/marketplace-manifest/decisions.md new file mode 100644 index 0000000..fa9e3fe --- /dev/null +++ b/.sisyphus/notepads/marketplace-manifest/decisions.md @@ -0,0 +1 @@ +Decision: Use per-task notepads to capture learnings and outcomes for marketplace manifest task. diff --git a/.sisyphus/notepads/marketplace-manifest/issues.md b/.sisyphus/notepads/marketplace-manifest/issues.md new file mode 100644 index 0000000..676f21a --- /dev/null +++ b/.sisyphus/notepads/marketplace-manifest/issues.md @@ -0,0 +1 @@ +- None yet. diff --git a/.sisyphus/notepads/marketplace-manifest/learnings.md b/.sisyphus/notepads/marketplace-manifest/learnings.md new file mode 100644 index 0000000..ba5eac1 --- /dev/null +++ b/.sisyphus/notepads/marketplace-manifest/learnings.md @@ -0,0 +1 @@ +Task marketplace manifest: created marketplace.json for Claude plugin integration (task-7-marketplace). diff --git a/.sisyphus/notepads/marketplace-manifest/problems.md b/.sisyphus/notepads/marketplace-manifest/problems.md new file mode 100644 index 0000000..8516c1f --- /dev/null +++ b/.sisyphus/notepads/marketplace-manifest/problems.md @@ -0,0 +1 @@ +Problems: None identified so far for marketplace manifest task. From d6efd524b2837e7621aa276dd3cafde36df9700a Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 07:00:23 -0700 Subject: [PATCH 10/17] ci: add disabled npm trusted publishing scaffold (#7488) --- .github/workflows/npm-publish.yml | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/npm-publish.yml diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..ddfb760 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,56 @@ +# npm Trusted Publishing Scaffold +# +# STATUS: DISABLED — manual workflow_dispatch only +# +# HOW TO ENABLE (do this in order): +# 1. First publish must be done MANUALLY: +# npm login +# cd packages/skills-cli +# npm publish --access public +# 2. After first publish, configure Trusted Publisher on npmjs.com: +# Package: @gravity/skills +# Org: gravityforms +# Repo: gravityskills +# Workflow: npm-publish.yml +# 3. This workflow can then be triggered manually for subsequent releases. +# WHY NO NPM_TOKEN: Trusted publishing uses GitHub OIDC (id-token: write) +# to mint a short-lived publish token at runtime. No stored secrets needed. +# See: https://docs.npmjs.com/trusted-publishers/ + +name: Publish to npm + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 11.0.8 + + - uses: actions/setup-node@v4 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build CLI + run: pnpm --filter @gravity/skills build + + - name: Test CLI + run: pnpm --filter @gravity/skills test + + - name: Publish to npm + run: npm publish --access public + working-directory: packages/skills-cli From a64779d85645fd6e7130eea9693c10dbe9e283ec Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 07:00:29 -0700 Subject: [PATCH 11/17] ci: add release artifact workflow (#7488) --- .github/workflows/release.yml | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c87b7a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 11.0.8 + + - uses: actions/setup-node@v4 + with: + node-version: '24.11.1' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test + + - name: Validate skills + run: pnpm skills:validate + + - name: Pack skills + run: pnpm skills:pack + + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ github.ref_name }}" \ + dist/skills/*.zip \ + --title "${{ github.ref_name }}" \ + --generate-notes From bb0421120c6aee5fe2caff5400505cd8d66637b4 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 07:01:10 -0700 Subject: [PATCH 12/17] ci: add validation workflow (#7488) --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4a8e076 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 11.0.8 + + - uses: actions/setup-node@v4 + with: + node-version: '24.11.1' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test + + - name: Validate skills + run: pnpm skills:validate + + - name: Pack skills + run: pnpm skills:pack + + - name: Verify zip integrity + run: | + for zip in dist/skills/*.zip; do + echo "Verifying $zip" + unzip -t "$zip" + done From 397c86785f61ae313246afb0f8583bf917a7aa83 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 07:11:04 -0700 Subject: [PATCH 13/17] chore: remove .sisyphus/ from tracking (#7488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Files were committed before .gitignore rule was in place. The .gitignore entry already exists — this commit just removes the tracked files so they stay local-only. --- .sisyphus/boulder.json | 23 - .../marketplace-manifest/decisions.md | 1 - .../notepads/marketplace-manifest/issues.md | 1 - .../marketplace-manifest/learnings.md | 1 - .../notepads/marketplace-manifest/problems.md | 1 - .../skills-repo-framework/learnings.md | 38 - .sisyphus/plans/skills-repo-framework.md | 1098 ----------------- 7 files changed, 1163 deletions(-) delete mode 100644 .sisyphus/boulder.json delete mode 100644 .sisyphus/notepads/marketplace-manifest/decisions.md delete mode 100644 .sisyphus/notepads/marketplace-manifest/issues.md delete mode 100644 .sisyphus/notepads/marketplace-manifest/learnings.md delete mode 100644 .sisyphus/notepads/marketplace-manifest/problems.md delete mode 100644 .sisyphus/notepads/skills-repo-framework/learnings.md delete mode 100644 .sisyphus/plans/skills-repo-framework.md diff --git a/.sisyphus/boulder.json b/.sisyphus/boulder.json deleted file mode 100644 index 36ce867..0000000 --- a/.sisyphus/boulder.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "active_plan": "/Users/faction23/Projects/gravity/gravityskills/.sisyphus/plans/skills-repo-framework.md", - "started_at": "2026-05-13T13:05:31.787Z", - "session_ids": [ - "ses_1deaab1ceffezTzEEWrUlWMQbr", - "ses_1de8e267affePUc0rgTpYoD5dH", - "ses_1de85de77ffeX8NI6g4Y61hj2w", - "ses_1de8615a0ffeN54729IBEdtQwG" - ], - "plan_name": "skills-repo-framework", - "agent": "atlas", - "task_sessions": { - "todo:1": { - "task_key": "todo:1", - "task_label": "1", - "task_title": "Runtime + pnpm Workspace Foundation", - "session_id": "ses_1de8615a0ffeN54729IBEdtQwG", - "agent": "Sisyphus-Junior", - "category": "quick", - "updated_at": "2026-05-13T13:18:56.475Z" - } - } -} \ No newline at end of file diff --git a/.sisyphus/notepads/marketplace-manifest/decisions.md b/.sisyphus/notepads/marketplace-manifest/decisions.md deleted file mode 100644 index fa9e3fe..0000000 --- a/.sisyphus/notepads/marketplace-manifest/decisions.md +++ /dev/null @@ -1 +0,0 @@ -Decision: Use per-task notepads to capture learnings and outcomes for marketplace manifest task. diff --git a/.sisyphus/notepads/marketplace-manifest/issues.md b/.sisyphus/notepads/marketplace-manifest/issues.md deleted file mode 100644 index 676f21a..0000000 --- a/.sisyphus/notepads/marketplace-manifest/issues.md +++ /dev/null @@ -1 +0,0 @@ -- None yet. diff --git a/.sisyphus/notepads/marketplace-manifest/learnings.md b/.sisyphus/notepads/marketplace-manifest/learnings.md deleted file mode 100644 index ba5eac1..0000000 --- a/.sisyphus/notepads/marketplace-manifest/learnings.md +++ /dev/null @@ -1 +0,0 @@ -Task marketplace manifest: created marketplace.json for Claude plugin integration (task-7-marketplace). diff --git a/.sisyphus/notepads/marketplace-manifest/problems.md b/.sisyphus/notepads/marketplace-manifest/problems.md deleted file mode 100644 index 8516c1f..0000000 --- a/.sisyphus/notepads/marketplace-manifest/problems.md +++ /dev/null @@ -1 +0,0 @@ -Problems: None identified so far for marketplace manifest task. diff --git a/.sisyphus/notepads/skills-repo-framework/learnings.md b/.sisyphus/notepads/skills-repo-framework/learnings.md deleted file mode 100644 index 0d85d39..0000000 --- a/.sisyphus/notepads/skills-repo-framework/learnings.md +++ /dev/null @@ -1,38 +0,0 @@ -# Learnings - -## [Wave 1] Foundation - -- `packageManager: pnpm@11.0.8` in root package.json — Corepack auto-install -- `pnpm-workspace.yaml` has supply chain hardening: minimumReleaseAge: 1440, blockExoticSubdeps: true, trustPolicy: no-downgrade, verifyDepsBeforeRun: install -- `skills:validate` script: `node --import tsx/esm scripts/validate-skills.ts` -- `skills:pack` script: `node --import tsx/esm scripts/pack-skills.ts` -- `build` and `test` scripts use `pnpm -r` (recursive workspace) -- `skills/placeholder-skill/SKILL.md` exists with valid frontmatter (name: placeholder-skill, description, license, metadata) -- `skills/placeholder-skill/references/README.md` exists for zip packaging validation -- Node 24.11.1 pinned in `.nvmrc` -- GPL-2.0+ LICENSE at root -- README.md is minimal (52 bytes) — was NOT updated with 3 install methods yet (T2 commit may be missing from log) -- Git log shows 5 commits; T2 (license+README) commit message not visible — LICENSE file IS present though -- `packages/` directory does NOT exist yet (CLI scaffold coming in T6) -- `.claude-plugin/` directory does NOT exist yet (T7) -- `scripts/` directory does NOT exist yet (T4, T5) -- `.github/workflows/` does NOT exist yet (T8, T9, T10) -- `tsx` is needed as devDep for running TypeScript scripts via `node --import tsx/esm` - -## Key Constraints -- ZERO npm dependencies in `packages/skills-cli/` published package -- No Turborepo, Nx, Changesets -- No postinstall scripts -- No actual npm publishing -- No real product skills — placeholder only -- bun test for testing -- All commits reference (#7488) - -## Task 5 pack skills - 2026-05-13 -- Root skills:pack script runs through tsx/esm and Node 24 when implemented as CommonJS-style TypeScript, matching existing validate-skills.ts. -- Packaging from skills/ as cwd preserves the skill directory as zip root while preventing files outside the skill directory from being included. - -## Task 6 - @gravity/skills CLI scaffold -- Added packages/skills-cli as @gravity/skills with explicit empty dependencies and dev-only TypeScript/Bun types. -- CLI uses manual argv parsing and Node global fetch; no commander/yargs/oclif. -- pnpm-workspace allowBuilds must set esbuild: false so verifyDepsBeforeRun does not fail on ignored transitive tsx build scripts. diff --git a/.sisyphus/plans/skills-repo-framework.md b/.sisyphus/plans/skills-repo-framework.md deleted file mode 100644 index 539d720..0000000 --- a/.sisyphus/plans/skills-repo-framework.md +++ /dev/null @@ -1,1098 +0,0 @@ -# Gravity Skills Repo Framework - -## TL;DR - -> **Quick Summary**: Set up the public skills repository framework for Gravity products with three distribution channels — GitHub Releases (zip artifacts), npm CLI installer (`@gravity/skills`), and Claude Code plugin marketplace — using pnpm 11 with full supply chain hardening. -> -> **Deliverables**: -> - pnpm 11 monorepo with supply chain hardening (minimumReleaseAge, blockExoticSubdeps, trustPolicy) -> - `skills/` directory with placeholder skill following Agent Skills spec -> - `packages/skills-cli/` with `@gravity/skills` CLI scaffold (`--help`, `install `) -> - `.claude-plugin/marketplace.json` for native Claude Code plugin install -> - GitHub Actions: CI validation workflow + tag-triggered release workflow + disabled npm publish scaffold -> - Skill validation script (frontmatter, naming, structure) -> - Zip packaging script (deterministic, excludes junk files) -> - GPL-2.0+ LICENSE -> -> **Estimated Effort**: Medium -> **Parallel Execution**: YES - 3 waves -> **Critical Path**: Task 1 → Task 3 → Task 5 → Task 7 → Task 9 - ---- - -## Context - -### Original Request -Set up a public skills repo framework for Gravity products (Gravity Forms, add-ons, Gravity SMTP, etc.). The repo (`gravityforms/gravityskills`) is currently private and empty. It needs to serve skills via zip downloads from GitHub Releases, an npm CLI installer under `@gravity/skills` using trusted publishing only, and Claude Code plugin marketplace integration. No actual publishing happens in this branch due to npm supply chain concerns. - -### Interview Summary -**Key Discussions**: -- **Zip distribution**: CI generates zips on tag, attaches to GitHub Releases (not committed to repo like Vercel) -- **Claude plugin marketplace**: Yes — `.claude-plugin/marketplace.json`, no approval process needed -- **npm package**: CLI installer (`npx @gravity/skills install `), scaffold only, no publish yet -- **Repo structure**: `skills/` + `packages/` (Vercel/Anthropic pattern) -- **pnpm version**: v11 (supply chain hardening on by default) -- **Node**: 24.11.1, npm: 11.6.2 -- **License**: GPL-2.0+ -- **First skills**: Placeholder/test skill only -- **Testing**: Basic CI validation (frontmatter, zip integrity, build) -- **Branch protection**: Not in this branch - -**Research Findings**: -- Vercel ships skill dirs + zip siblings in repo, uses per-skill CI workflows -- Anthropic uses `.claude-plugin/marketplace.json` with plugins grouping skills into installable bundles -- Agent Skills spec: `SKILL.md` with YAML frontmatter (name, description required), optional scripts/references/assets dirs -- npm trusted publishing: OIDC-based, requires npm 11.5.1+, first publish must be manual, then configure OIDC on npmjs.com -- pnpm 11 defaults: `minimumReleaseAge: 1440`, `blockExoticSubdeps: true`, `verifyDepsBeforeRun: "install"`, postinstall disabled - -### Metis Review -**Identified Gaps** (addressed): -- `.agents/` vs `skills/` canonical location: `.agents/` is internal/gitignored, `skills/` is the public directory — no conflict -- Zip structure: each skill zipped as `/SKILL.md` (not flat) -- CLI minimum behavior for this branch: `--help` + `install ` stub that downloads from GitHub Releases -- Release tagging: manual tags only for this branch, no changesets -- Exclude `.DS_Store`, `node_modules`, `.git`, `.env` from zips -- Placeholder skill clearly marked as test-only -- npm trusted publishing workflow disabled by default, with documentation on manual first-publish requirement - ---- - -## Work Objectives - -### Core Objective -Establish the complete framework for a public skills repository with three distribution channels, validated by CI, with no actual publishing. - -### Concrete Deliverables -- Root `package.json` with `packageManager` field, `.nvmrc`, `pnpm-workspace.yaml` -- `LICENSE` (GPL-2.0+) -- `skills/placeholder-skill/SKILL.md` following Agent Skills spec -- `packages/skills-cli/` — minimal TypeScript CLI package (`@gravity/skills`) -- `.claude-plugin/marketplace.json` -- `.github/workflows/ci.yml` — validation on PR/push -- `.github/workflows/release.yml` — zip generation + GitHub Release on tags -- `.github/workflows/npm-publish.yml` — disabled scaffold for future trusted publishing -- `scripts/validate-skills.ts` — frontmatter + structure validation -- `scripts/pack-skills.ts` — deterministic zip generation -- Updated `README.md` with installation instructions for all three channels -- Updated `.gitignore` for build artifacts - -### Definition of Done -- [ ] `pnpm install --frozen-lockfile` exits 0 -- [ ] `pnpm build` exits 0 -- [ ] `pnpm test` exits 0 -- [ ] `pnpm skills:validate` exits 0 -- [ ] `pnpm skills:pack` exits 0 and produces valid zips -- [ ] CLI `--help` works -- [ ] All GitHub Actions workflow files pass `actionlint` - -### Must Have -- pnpm 11 with explicit supply chain config -- Node 24.11.1 pinned in `.nvmrc` -- `packageManager` field in root `package.json` -- GPL-2.0+ LICENSE at root -- At least one skill following Agent Skills spec -- Skill validation (frontmatter required fields, naming convention, structure) -- Zip packaging that excludes junk files -- CI workflow that runs on PR -- Release workflow that generates zips on tag -- Claude plugin marketplace manifest - -### Must NOT Have (Guardrails) -- NO actual npm publishing — workflow exists but is disabled/manual-only -- NO real product skills — placeholder only -- NO migration of `.agents/` content (those are internal, gitignored) -- NO branch protection setup (done manually later) -- NO docs site or catalog website -- NO Changesets, Turborepo, Nx, or complex monorepo tooling -- NO postinstall scripts in any package -- NO long-lived npm tokens or secrets for CI validation -- NO duplicate skill sources between `.agents/` and `skills/` -- NO over-abstracted CLI — minimal install command, not a full SDK - ---- - -## Verification Strategy (MANDATORY) - -> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed. No exceptions. - -### Test Decision -- **Infrastructure exists**: NO (new repo) -- **Automated tests**: YES (tests-after) — validation scripts + unit tests for CLI -- **Framework**: `bun test` (ships with bun, zero config, fast) - -### QA Policy -Every task MUST include agent-executed QA scenarios. -Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`. - -- **CLI**: Use Bash — run command, validate output, check exit code -- **Scripts**: Use Bash — run validation/pack scripts, check outputs -- **CI Workflows**: Use `actionlint` for syntax validation -- **Zips**: Use `unzip -t` and `zipinfo` for integrity/contents - ---- - -## Execution Strategy - -### Pre-Implementation: Issue + Spec Creation - -> This plan requires a GitHub issue and spec BEFORE implementation begins. -> The orchestrator must follow this sequence with user checkpoints. - -**Step A: Create GitHub Issue** -- Use `.agents/github-issue-creator` skill to create issue in `gravityforms/backlog` -- Team: Orion, Label: `gravity`, Assignee: `faction23` -- Type: Feature request -- Title: `Gravity: Public skills repository framework` -- Body: Dense summary of this plan's objectives, deliverables, and three distribution channels -- **PAUSE after creation** — user reviews issue on GitHub before proceeding - -**Step B: Create Spec (after user confirms issue)** -- Use `.agents/skill-creator` or spec skill to create `.specs/skills-repo-framework.md` -- Link spec's `source:` field to the created issue URL -- Spec captures: objectives, architecture decisions, distribution channels, supply chain strategy -- **PAUSE after creation** — user reviews spec before implementation begins - -**Step C: Begin Implementation** -- Create feature branch from `main` -- Execute Wave 1-3 tasks below -- All commits reference the issue number - -### Parallel Execution Waves - -``` -Wave 1 (Start Immediately - foundation): -├── Task 1: Runtime + pnpm workspace foundation [quick] -├── Task 2: License + README scaffold [quick] -├── Task 3: Placeholder skill [quick] - -Wave 2 (After Wave 1 - tooling + packages): -├── Task 4: Skill validation script [unspecified-high] -├── Task 5: Zip packaging script [unspecified-high] -├── Task 6: CLI package scaffold [unspecified-high] -├── Task 7: Claude plugin marketplace manifest [quick] - -Wave 3 (After Wave 2 - CI + release): -├── Task 8: CI validation workflow [unspecified-high] -├── Task 9: Release artifact workflow [unspecified-high] -├── Task 10: npm trusted publishing scaffold [quick] - -Wave FINAL (After ALL tasks): -├── F1: Plan compliance audit (oracle) -├── F2: Code quality review (unspecified-high) -├── F3: Real manual QA (unspecified-high) -├── F4: Scope fidelity check (deep) --> Present results -> Get explicit user okay -``` - -### Dependency Matrix - -| Task | Depends On | Blocks | -|------|-----------|--------| -| 1 | - | 4, 5, 6, 7, 8, 9, 10 | -| 2 | - | 8 | -| 3 | - | 4, 5, 7, 8 | -| 4 | 1, 3 | 8 | -| 5 | 1, 3 | 8, 9 | -| 6 | 1 | 8, 10 | -| 7 | 1, 3 | 8 | -| 8 | 4, 5, 6, 7 | F1-F4 | -| 9 | 5 | F1-F4 | -| 10 | 6 | F1-F4 | - -### Agent Dispatch Summary - -- **Wave 1**: 3 tasks — T1 `quick`, T2 `quick`, T3 `quick` -- **Wave 2**: 4 tasks — T4 `unspecified-high`, T5 `unspecified-high`, T6 `unspecified-high`, T7 `quick` -- **Wave 3**: 3 tasks — T8 `unspecified-high`, T9 `unspecified-high`, T10 `quick` -- **FINAL**: 4 tasks — F1 `oracle`, F2 `unspecified-high`, F3 `unspecified-high`, F4 `deep` - ---- - -## TODOs - -- [ ] 1. Runtime + pnpm Workspace Foundation - - **What to do**: - - Create `.nvmrc` with `24.11.1` - - Create root `package.json` with: - - `"name": "gravityskills"` (private, not published) - - `"private": true` - - `"packageManager": "pnpm@11.0.8"` (latest stable pnpm 11) - - `"engines": { "node": "24.11.1", "npm": "11.6.2" }` - - Scripts: `build`, `test`, `skills:validate`, `skills:pack` - - Create `pnpm-workspace.yaml` with: - - `packages: ["packages/*"]` - - `minimumReleaseAge: 1440` (1 day — pnpm 11 default, but be explicit) - - `blockExoticSubdeps: true` (pnpm 11 default, be explicit) - - `trustPolicy: no-downgrade` - - `verifyDepsBeforeRun: install` - - Run `pnpm install` to generate lockfile - - Update `.gitignore` to include: `node_modules/`, `dist/`, `.DS_Store`, `*.tgz`, `.sisyphus/` - - **Must NOT do**: - - Do not add Turborepo, Nx, Changesets, or any complex monorepo tooling - - Do not add postinstall scripts - - Do not add unnecessary dependencies - - **Recommended Agent Profile**: - - **Category**: `quick` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES - - **Parallel Group**: Wave 1 (with Tasks 2, 3) - - **Blocks**: Tasks 4, 5, 6, 7, 8, 9, 10 - - **Blocked By**: None - - **References**: - - **Pattern References**: - - pnpm 11 supply chain docs: https://pnpm.io/supply-chain-security — minimumReleaseAge, blockExoticSubdeps, trustPolicy settings - - pnpm workspace config: https://pnpm.io/pnpm-workspace_yaml — workspace package glob and settings format - - **API/Type References**: - - `packageManager` field docs: https://nodejs.org/api/packages.html#packagemanager — enables Corepack auto-install - - **External References**: - - pnpm 11.0.8 release: https://github.com/pnpm/pnpm/releases/tag/v11.0.8 — latest stable, fixes self-update and tarball URL issues - - **Acceptance Criteria**: - - [ ] `.nvmrc` contains `24.11.1` - - [ ] `package.json` has `packageManager` field starting with `pnpm@11` - - [ ] `pnpm-workspace.yaml` has explicit `minimumReleaseAge: 1440` - - [ ] `pnpm install --frozen-lockfile` exits 0 - - [ ] `pnpm-lock.yaml` exists and is committed - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: pnpm install succeeds with frozen lockfile - Tool: Bash - Preconditions: Clean checkout, Node 24.11.1 available - Steps: - 1. Run `cat .nvmrc` → expect output `24.11.1` - 2. Run `node -e "console.log(JSON.parse(require('fs').readFileSync('package.json','utf8')).packageManager)"` → expect starts with `pnpm@11` - 3. Run `pnpm install --frozen-lockfile` → expect exit 0 - 4. Run `grep minimumReleaseAge pnpm-workspace.yaml` → expect `1440` - 5. Run `grep blockExoticSubdeps pnpm-workspace.yaml` → expect `true` - Expected Result: All commands exit 0 with correct values - Failure Indicators: pnpm install fails, missing config fields - Evidence: .sisyphus/evidence/task-1-pnpm-foundation.txt - - Scenario: .gitignore excludes build artifacts and internal dirs - Tool: Bash - Preconditions: .gitignore exists - Steps: - 1. Run `grep -c 'node_modules' .gitignore` → expect ≥ 1 - 2. Run `grep -c 'dist/' .gitignore` → expect ≥ 1 - 3. Run `grep -c '.DS_Store' .gitignore` → expect ≥ 1 - 4. Run `grep -c '.sisyphus/' .gitignore` → expect ≥ 1 - Expected Result: All patterns present including .sisyphus/ - Failure Indicators: Missing patterns in .gitignore - Evidence: .sisyphus/evidence/task-1-gitignore.txt - ``` - - **Commit**: YES - - Message: `chore: add runtime and pnpm workspace foundation` - - Files: `.nvmrc`, `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `.gitignore` - -- [ ] 2. License + README Scaffold - - **What to do**: - - Add `LICENSE` file with GPL-2.0+ full text - - Update `README.md` with: - - Project description (skills for Gravity products) - - Three installation methods: Claude Code plugin (`/plugin marketplace add`), CLI (`npx @gravity/skills install`), direct download (GitHub Releases) - - Contributing section (brief, point to Agent Skills spec) - - License badge - - Note that npm publishing is not yet active - - **Must NOT do**: - - Do not create a docs site - - Do not add CONTRIBUTING.md or CODE_OF_CONDUCT.md yet - - Do not mention internal tooling or `.agents/` - - **Recommended Agent Profile**: - - **Category**: `quick` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES - - **Parallel Group**: Wave 1 (with Tasks 1, 3) - - **Blocks**: Task 8 - - **Blocked By**: None - - **References**: - - **External References**: - - GPL-2.0 license text: https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt - - Agent Skills spec: https://agentskills.io/specification — link for contributors - - Anthropic README example: https://github.com/anthropics/skills/blob/main/README.md — installation section pattern - - **Acceptance Criteria**: - - [ ] `LICENSE` file exists with GPL-2.0 text - - [ ] `README.md` mentions all three installation methods - - [ ] `README.md` does not reference `.agents/` or internal tooling - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: License file is valid GPL-2.0 - Tool: Bash - Preconditions: LICENSE exists - Steps: - 1. Run `head -1 LICENSE` → expect contains "GNU GENERAL PUBLIC LICENSE" - 2. Run `grep -c 'Version 2' LICENSE` → expect ≥ 1 - Expected Result: GPL-2.0 identified - Failure Indicators: Wrong license text or missing file - Evidence: .sisyphus/evidence/task-2-license.txt - - Scenario: README has all three install methods - Tool: Bash - Preconditions: README.md exists - Steps: - 1. Run `grep -c 'plugin marketplace add' README.md` → expect ≥ 1 - 2. Run `grep -c '@gravity/skills' README.md` → expect ≥ 1 - 3. Run `grep -c 'Releases' README.md` → expect ≥ 1 - 4. Run `grep -c '.agents' README.md` → expect 0 - Expected Result: All install methods documented, no internal references - Failure Indicators: Missing install method or internal leakage - Evidence: .sisyphus/evidence/task-2-readme.txt - ``` - - **Commit**: YES - - Message: `chore: add GPL-2.0+ license and README scaffold` - - Files: `LICENSE`, `README.md` - -- [ ] 3. Placeholder Skill - - **What to do**: - - Create `skills/placeholder-skill/SKILL.md` following Agent Skills spec: - ```yaml - --- - name: placeholder-skill - description: A placeholder skill for testing the Gravity Skills repository framework. Not intended for production use. - license: GPL-2.0+ - metadata: - author: gravityforms - version: "0.0.1" - --- - ``` - - Add markdown body with brief instructions explaining this is a test/placeholder - - Optionally add a `references/` dir with a sample reference file to validate zip packaging - - **Must NOT do**: - - Do not create any real Gravity product skills - - Do not include any proprietary Gravity code or documentation - - **Recommended Agent Profile**: - - **Category**: `quick` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES - - **Parallel Group**: Wave 1 (with Tasks 1, 2) - - **Blocks**: Tasks 4, 5, 7 - - **Blocked By**: None - - **References**: - - **Pattern References**: - - Agent Skills spec: https://agentskills.io/specification — SKILL.md frontmatter format, required fields, naming rules - - Anthropic template: https://github.com/anthropics/skills/blob/main/template/SKILL.md — minimal skill template - - Vercel skill example: `skills/deploy-to-vercel/SKILL.md` in `vercel-labs/agent-skills` — structure with references dir - - **Acceptance Criteria**: - - [ ] `skills/placeholder-skill/SKILL.md` exists - - [ ] Frontmatter has required `name` and `description` fields - - [ ] `name` matches parent directory name (`placeholder-skill`) - - [ ] `name` is lowercase, hyphens only, no consecutive hyphens - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: Placeholder skill follows Agent Skills spec - Tool: Bash - Preconditions: skills/placeholder-skill/SKILL.md exists - Steps: - 1. Run `test -f skills/placeholder-skill/SKILL.md` → expect exit 0 - 2. Run `head -2 skills/placeholder-skill/SKILL.md` → expect first line is `---` - 3. Run `grep 'name: placeholder-skill' skills/placeholder-skill/SKILL.md` → expect match - 4. Run `grep 'description:' skills/placeholder-skill/SKILL.md` → expect match - Expected Result: Valid SKILL.md with required frontmatter - Failure Indicators: Missing file, missing frontmatter, name mismatch - Evidence: .sisyphus/evidence/task-3-placeholder-skill.txt - - Scenario: Skill name matches directory name - Tool: Bash - Preconditions: Skill directory exists - Steps: - 1. Extract name from SKILL.md frontmatter - 2. Compare with parent directory name `placeholder-skill` - Expected Result: Names match exactly - Evidence: .sisyphus/evidence/task-3-name-match.txt - ``` - - **Commit**: YES - - Message: `feat: add placeholder skill following Agent Skills spec` - - Files: `skills/placeholder-skill/SKILL.md`, optionally `skills/placeholder-skill/references/` - -- [ ] 4. Skill Validation Script - - **What to do**: - - Create `scripts/validate-skills.ts` that: - - Scans `skills/*/SKILL.md` for all skills - - Parses YAML frontmatter (use `yaml` package or manual parsing) - - Validates required fields: `name`, `description` - - Validates `name` format: lowercase, hyphens only, 1-64 chars, no consecutive hyphens, no leading/trailing hyphens - - Validates `name` matches parent directory name - - Checks for duplicate skill names - - Checks `description` is 1-1024 characters - - Reports errors with file paths and exits non-zero on failure - - Add a test file `scripts/__tests__/validate-skills.test.ts` with: - - Test for valid skill passing - - Test for missing `name` field - - Test for invalid `name` format (uppercase, spaces, consecutive hyphens) - - Test for name/directory mismatch - - Test for duplicate names - - Wire up as `pnpm skills:validate` in root package.json - - **Must NOT do**: - - Do not add heavy validation frameworks (no ajv, zod, etc.) - - Do not validate optional fields beyond basic type checks - - Do not fetch or validate external references - - **Recommended Agent Profile**: - - **Category**: `unspecified-high` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES - - **Parallel Group**: Wave 2 (with Tasks 5, 6, 7) - - **Blocks**: Task 8 - - **Blocked By**: Tasks 1, 3 - - **References**: - - **Pattern References**: - - Agent Skills spec naming rules: https://agentskills.io/specification — `name` field constraints (lowercase, hyphens, 1-64 chars, must match parent dir) - - `skills/placeholder-skill/SKILL.md` — the skill to validate against (created in Task 3) - - **External References**: - - YAML frontmatter parsing: standard `---` delimited block at top of file, parse with simple regex or `yaml` npm package - - **WHY Each Reference Matters**: - - The spec defines exact validation rules — don't invent constraints, follow the spec precisely - - The placeholder skill is the test fixture — validation must pass on it - - **Acceptance Criteria**: - - [ ] `scripts/validate-skills.ts` exists - - [ ] `pnpm skills:validate` exits 0 with placeholder skill present - - [ ] Validation catches: missing name, invalid name format, name/dir mismatch, missing description - - [ ] `bun test scripts/__tests__/validate-skills.test.ts` passes - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: Validation passes for valid placeholder skill - Tool: Bash - Preconditions: placeholder-skill exists with valid SKILL.md - Steps: - 1. Run `pnpm skills:validate` - 2. Check exit code → expect 0 - 3. Check output contains "placeholder-skill" as validated - Expected Result: Clean validation pass - Failure Indicators: Non-zero exit, error messages - Evidence: .sisyphus/evidence/task-4-validate-pass.txt - - Scenario: Validation fails for invalid skill - Tool: Bash - Preconditions: Create temp invalid skill with uppercase name - Steps: - 1. Create `skills/Invalid-Skill/SKILL.md` with `name: Invalid-Skill` - 2. Run `pnpm skills:validate` - 3. Check exit code → expect non-zero - 4. Check output mentions invalid name format - 5. Remove temp skill - Expected Result: Validation catches the invalid name - Failure Indicators: Validation passes when it shouldn't - Evidence: .sisyphus/evidence/task-4-validate-fail.txt - ``` - - **Commit**: YES - - Message: `feat: add skill validation script` - - Files: `scripts/validate-skills.ts`, `scripts/__tests__/validate-skills.test.ts`, `package.json` (script entry) - -- [ ] 5. Zip Packaging Script - - **What to do**: - - Create `scripts/pack-skills.ts` that: - - Scans `skills/*/` for all skill directories - - Creates a `dist/skills/` output directory - - For each skill, creates `.zip` containing the skill directory (not flat — zip root is the skill dir name) - - Excludes: `node_modules/`, `.git/`, `.DS_Store`, `.env`, `*.tgz`, `__pycache__/` - - Uses Node's built-in `child_process` to call `zip` or uses `archiver` npm package - - Logs each zip created with size - - Exits non-zero if any zip fails - - Add test file `scripts/__tests__/pack-skills.test.ts`: - - Test zip is created for placeholder skill - - Test zip contains SKILL.md - - Test zip excludes .DS_Store - - Test zip root is skill directory name (not flat) - - Wire up as `pnpm skills:pack` in root package.json - - **Must NOT do**: - - Do not commit zips to the repo (CI generates them) - - Do not create one monolithic zip of all skills - - Do not include any files from outside the skill directory - - **Recommended Agent Profile**: - - **Category**: `unspecified-high` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES - - **Parallel Group**: Wave 2 (with Tasks 4, 6, 7) - - **Blocks**: Tasks 8, 9 - - **Blocked By**: Tasks 1, 3 - - **References**: - - **Pattern References**: - - Vercel zip pattern: `vercel-labs/agent-skills` has `Archive.zip` inside each skill dir + `.zip` at skills/ root — we do NOT follow this (we generate to `dist/`) - - **External References**: - - `archiver` npm package: https://www.npmjs.com/package/archiver — if avoiding shell `zip` dependency - - Alternative: Node `child_process.execSync('zip ...')` — simpler if `zip` is available in CI (ubuntu-latest has it) - - **Acceptance Criteria**: - - [ ] `scripts/pack-skills.ts` exists - - [ ] `pnpm skills:pack` exits 0 - - [ ] `dist/skills/placeholder-skill.zip` is created - - [ ] `unzip -t dist/skills/placeholder-skill.zip` exits 0 - - [ ] `zipinfo -1 dist/skills/placeholder-skill.zip` includes `placeholder-skill/SKILL.md` - - [ ] `zipinfo -1 dist/skills/placeholder-skill.zip` does NOT include `.DS_Store` or `node_modules` - - [ ] `bun test scripts/__tests__/pack-skills.test.ts` passes - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: Zip packaging creates valid archive - Tool: Bash - Preconditions: placeholder-skill exists, dist/ is clean - Steps: - 1. Run `rm -rf dist/` - 2. Run `pnpm skills:pack` → expect exit 0 - 3. Run `test -f dist/skills/placeholder-skill.zip` → expect exit 0 - 4. Run `unzip -t dist/skills/placeholder-skill.zip` → expect exit 0 - 5. Run `zipinfo -1 dist/skills/placeholder-skill.zip` → expect contains `placeholder-skill/SKILL.md` - Expected Result: Valid zip with correct structure - Failure Indicators: Missing zip, corrupt archive, wrong structure - Evidence: .sisyphus/evidence/task-5-pack-valid.txt - - Scenario: Zip excludes junk files - Tool: Bash - Preconditions: Create .DS_Store in skill dir - Steps: - 1. Run `touch skills/placeholder-skill/.DS_Store` - 2. Run `pnpm skills:pack` - 3. Run `zipinfo -1 dist/skills/placeholder-skill.zip | grep -c '.DS_Store'` → expect 0 - 4. Run `rm skills/placeholder-skill/.DS_Store` - Expected Result: Junk files excluded from zip - Failure Indicators: .DS_Store appears in zip contents - Evidence: .sisyphus/evidence/task-5-pack-excludes.txt - ``` - - **Commit**: YES - - Message: `feat: add skill zip packaging script` - - Files: `scripts/pack-skills.ts`, `scripts/__tests__/pack-skills.test.ts`, `package.json` (script entry) - -- [ ] 6. @gravity/skills CLI Package Scaffold - - **What to do**: - - Create `packages/skills-cli/` with: - - `package.json`: - - `"name": "@gravity/skills"` - - `"version": "0.0.1"` (not published yet) - - `"bin": { "gravity-skills": "./dist/index.js" }` - - `"type": "module"` - - `"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" }` - - `"license": "GPL-2.0+"` - - `"repository": { "type": "git", "url": "https://github.com/gravityforms/gravityskills" }` - - `"engines": { "node": ">=20" }` (broader than repo requirement for CLI users) - - Scripts: `build`, `test` - - `tsconfig.json` for TypeScript compilation - - `src/index.ts` — entry point with CLI argument parsing: - - `--help` / `-h`: Show usage (list skills, install skill) - - `--version` / `-v`: Show package version - - `install `: Download skill zip from GitHub Releases and extract to target dir - - `list`: List available skills (reads from a bundled or fetched manifest) - - `src/commands/install.ts` — download zip from `https://github.com/gravityforms/gravityskills/releases/latest/download/.zip`, extract to current dir or `--target` path - - `src/commands/list.ts` — fetch/display available skills - - Minimal deps: **ZERO npm dependencies**. Use only Node built-ins: - - `https` / `fetch` (Node 24 has global fetch) for downloading zips from GitHub Releases - - `fs` / `fs/promises` for file operations - - `path` for path manipulation - - `child_process` for `unzip` or use `zlib` + manual zip parsing - - `crypto` for integrity checks if needed - - `readline` for minimal interactive prompts if needed - - Parse YAML frontmatter with simple regex (3 lines: split on `---`, no yaml lib needed for reading metadata) - - The published package must have `"dependencies": {}` in package.json — absolutely no npm deps - - Build with bundler (rolldown, esbuild, or tsc) so the dist is a single self-contained file - - This makes the CLI resilient to npm supply chain attacks — nothing to compromise except our own code - - Test file `src/__tests__/cli.test.ts`: - - Test `--help` output - - Test `--version` output - - Test `install` with mock/stub (don't actually download) - - Run `pnpm install` from root to wire up workspace - - **Must NOT do**: - - Do not add heavy CLI frameworks (commander, yargs, oclif) - - Do not add ANY npm dependencies — the published package must have zero deps - - Do not implement authentication or private skill access - - Do not add SDK/programmatic API - - Do not publish to npm - - **Recommended Agent Profile**: - - **Category**: `unspecified-high` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES - - **Parallel Group**: Wave 2 (with Tasks 4, 5, 7) - - **Blocks**: Tasks 8, 10 - - **Blocked By**: Task 1 - - **References**: - - **Pattern References**: - - npm `bin` field: `package.json` `bin` maps CLI command name to entry file - - GitHub Releases download URL pattern: `https://github.com/{owner}/{repo}/releases/latest/download/{filename}` - - **External References**: - - npm trusted publishing: https://docs.npmjs.com/trusted-publishers/ — `publishConfig` setup for future OIDC publishing - - Node built-in `https` for downloads, `child_process` for `unzip` - - **WHY Each Reference Matters**: - - `publishConfig` must be correct now so the trusted publishing workflow works when enabled - - Download URL pattern is how the CLI fetches skills from GitHub Releases - - **Acceptance Criteria**: - - [ ] `packages/skills-cli/package.json` has `"name": "@gravity/skills"` - - [ ] `pnpm --filter @gravity/skills build` exits 0 - - [ ] `node packages/skills-cli/dist/index.js --help` exits 0 and shows usage - - [ ] `node packages/skills-cli/dist/index.js --version` shows `0.0.1` - - [ ] `node -e "const p=JSON.parse(require('fs').readFileSync('packages/skills-cli/package.json','utf8')); console.log(Object.keys(p.dependencies||{}).length)"` outputs `0` (zero dependencies) - - [ ] `bun test packages/skills-cli/src/__tests__/cli.test.ts` passes - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: CLI --help shows install command - Tool: Bash - Preconditions: CLI is built - Steps: - 1. Run `pnpm --filter @gravity/skills build` → expect exit 0 - 2. Run `node packages/skills-cli/dist/index.js --help` → expect exit 0 - 3. Check output contains "install" → expect match - 4. Check output contains "list" → expect match - Expected Result: Help shows available commands - Failure Indicators: Build fails, help missing commands - Evidence: .sisyphus/evidence/task-6-cli-help.txt - - Scenario: CLI --version shows correct version - Tool: Bash - Preconditions: CLI is built - Steps: - 1. Run `node packages/skills-cli/dist/index.js --version` - 2. Check output → expect `0.0.1` - Expected Result: Version matches package.json - Evidence: .sisyphus/evidence/task-6-cli-version.txt - ``` - - **Commit**: YES - - Message: `feat: scaffold @gravity/skills CLI package` - - Files: `packages/skills-cli/*`, `pnpm-lock.yaml` - -- [ ] 7. Claude Plugin Marketplace Manifest - - **What to do**: - - Create `.claude-plugin/marketplace.json` with: - ```json - { - "name": "gravity-skills", - "owner": { - "name": "Gravity Forms", - "email": "support@gravityforms.com" - }, - "metadata": { - "description": "Official skills for Gravity Forms, Gravity SMTP, and other Gravity products", - "version": "1.0.0" - }, - "plugins": [ - { - "name": "gravity-skills", - "description": "Skills for working with Gravity products", - "source": "./", - "strict": false, - "skills": [ - "./skills/placeholder-skill" - ] - } - ] - } - ``` - - Validate JSON is well-formed - - **Must NOT do**: - - Do not add real product skills to the plugins array yet - - Do not reference `.agents/` skills - - Do not invent skill paths that don't exist - - **Recommended Agent Profile**: - - **Category**: `quick` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES - - **Parallel Group**: Wave 2 (with Tasks 4, 5, 6) - - **Blocks**: Task 8 - - **Blocked By**: Tasks 1, 3 - - **References**: - - **Pattern References**: - - Anthropic marketplace.json: `anthropics/skills/.claude-plugin/marketplace.json` — exact schema to follow with `name`, `owner`, `metadata`, `plugins` array - - **Acceptance Criteria**: - - [ ] `.claude-plugin/marketplace.json` exists - - [ ] JSON is valid (`node -e "JSON.parse(require('fs').readFileSync('.claude-plugin/marketplace.json','utf8'))"` exits 0) - - [ ] `plugins[0].skills` references only existing skill directories - - [ ] `name` field is `"gravity-skills"` - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: Marketplace manifest is valid JSON with correct structure - Tool: Bash - Preconditions: .claude-plugin/marketplace.json exists - Steps: - 1. Run `node -e "const m=JSON.parse(require('fs').readFileSync('.claude-plugin/marketplace.json','utf8')); console.log(m.name)"` → expect `gravity-skills` - 2. Run `node -e "const m=JSON.parse(require('fs').readFileSync('.claude-plugin/marketplace.json','utf8')); console.log(m.plugins[0].skills[0])"` → expect `./skills/placeholder-skill` - 3. Run `test -d skills/placeholder-skill` → expect exit 0 - Expected Result: Valid manifest pointing to existing skills - Failure Indicators: Invalid JSON, missing skills, broken references - Evidence: .sisyphus/evidence/task-7-marketplace.txt - - Scenario: Referenced skill paths exist - Tool: Bash - Preconditions: Manifest exists - Steps: - 1. Parse all skill paths from plugins[].skills[] - 2. For each path, verify directory exists - Expected Result: All referenced paths exist - Evidence: .sisyphus/evidence/task-7-skill-refs.txt - ``` - - **Commit**: YES - - Message: `feat: add Claude plugin marketplace manifest` - - Files: `.claude-plugin/marketplace.json` - -- [ ] 8. CI Validation Workflow - - **What to do**: - - Create `.github/workflows/ci.yml`: - - Trigger: push to `main`, pull_request to `main` - - Jobs: - - `validate`: - - `runs-on: ubuntu-latest` - - Setup pnpm 11 via `pnpm/action-setup@v4` - - Setup Node 24.11.1 via `actions/setup-node@v4` with pnpm cache - - `pnpm install --frozen-lockfile` - - `pnpm build` - - `pnpm test` - - `pnpm skills:validate` - - `pnpm skills:pack` (verify zips can be created) - - Verify zips with `unzip -t dist/skills/*.zip` - - **Must NOT do**: - - Do not require any secrets for CI validation - - Do not publish anything - - Do not add complex matrix builds - - Do not add caching beyond pnpm's built-in - - **Recommended Agent Profile**: - - **Category**: `unspecified-high` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: NO - - **Parallel Group**: Wave 3 - - **Blocks**: F1-F4 - - **Blocked By**: Tasks 4, 5, 6, 7 - - **References**: - - **Pattern References**: - - Vercel CI workflow: `.github/workflows/react-best-practices-ci.yml` in `vercel-labs/agent-skills` — pnpm/action-setup + setup-node + validate + build pattern - - pnpm/action-setup@v4: https://github.com/pnpm/action-setup — official pnpm GitHub Action - - **External References**: - - actions/setup-node@v4 with pnpm cache: set `cache: 'pnpm'` and `cache-dependency-path: 'pnpm-lock.yaml'` - - **Acceptance Criteria**: - - [ ] `.github/workflows/ci.yml` exists - - [ ] Workflow triggers on push to main and PR to main - - [ ] Workflow runs: install, build, test, skills:validate, skills:pack - - [ ] No secrets required for validation jobs - - [ ] `actionlint .github/workflows/ci.yml` exits 0 (if actionlint available) - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: CI workflow YAML is valid - Tool: Bash - Preconditions: .github/workflows/ci.yml exists - Steps: - 1. Run `node -e "require('yaml').parse(require('fs').readFileSync('.github/workflows/ci.yml','utf8'))"` or `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"` → expect exit 0 - 2. Verify `on.push.branches` includes `main` - 3. Verify `on.pull_request.branches` includes `main` - 4. Verify steps include `pnpm install`, `pnpm build`, `pnpm test`, `pnpm skills:validate`, `pnpm skills:pack` - Expected Result: Valid workflow with all required steps - Failure Indicators: Invalid YAML, missing triggers, missing steps - Evidence: .sisyphus/evidence/task-8-ci-workflow.txt - - Scenario: No secrets referenced in CI workflow - Tool: Bash - Preconditions: ci.yml exists - Steps: - 1. Run `grep -c 'secrets\.' .github/workflows/ci.yml` → expect 0 - Expected Result: No secrets used - Evidence: .sisyphus/evidence/task-8-no-secrets.txt - ``` - - **Commit**: YES - - Message: `ci: add validation workflow` - - Files: `.github/workflows/ci.yml` - -- [ ] 9. Release Artifact Workflow - - **What to do**: - - Create `.github/workflows/release.yml`: - - Trigger: push tags matching `v*` (e.g. `v0.1.0`) - - Permissions: `contents: write` (to create releases) - - Jobs: - - `release`: - - `runs-on: ubuntu-latest` - - Setup pnpm + Node (same as CI) - - `pnpm install --frozen-lockfile` - - `pnpm build` - - `pnpm test` - - `pnpm skills:validate` - - `pnpm skills:pack` - - Create GitHub Release using `softprops/action-gh-release@v2` or `gh release create`: - - Upload all `dist/skills/*.zip` as release assets - - Generate release notes from tag - - Mark as latest - - **Must NOT do**: - - Do not publish to npm (separate workflow) - - Do not auto-create tags - - Do not create draft releases (make them actual releases) - - **Recommended Agent Profile**: - - **Category**: `unspecified-high` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES (with Task 10) - - **Parallel Group**: Wave 3 - - **Blocks**: F1-F4 - - **Blocked By**: Task 5 - - **References**: - - **External References**: - - `softprops/action-gh-release@v2`: https://github.com/softprops/action-gh-release — upload release assets with glob pattern - - Alternative: `gh release create $TAG dist/skills/*.zip --generate-notes` — simpler, uses GitHub CLI - - **Acceptance Criteria**: - - [ ] `.github/workflows/release.yml` exists - - [ ] Triggers only on `v*` tags - - [ ] Has `contents: write` permission - - [ ] Uploads `dist/skills/*.zip` as release assets - - [ ] Runs validation before creating release (install, build, test, validate, pack) - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: Release workflow triggers on version tags only - Tool: Bash - Preconditions: release.yml exists - Steps: - 1. Parse YAML and check `on.push.tags` → expect includes `v*` - 2. Verify no `on.push.branches` trigger - 3. Verify `permissions.contents` is `write` - Expected Result: Tag-only trigger with write permissions - Failure Indicators: Branch triggers, missing permissions - Evidence: .sisyphus/evidence/task-9-release-trigger.txt - - Scenario: Release workflow validates before creating release - Tool: Bash - Preconditions: release.yml exists - Steps: - 1. Check step order: install before build before test before validate before pack before release - 2. Verify release step references `dist/skills/*.zip` - Expected Result: Validation gates release creation - Evidence: .sisyphus/evidence/task-9-release-order.txt - ``` - - **Commit**: YES - - Message: `ci: add release artifact workflow` - - Files: `.github/workflows/release.yml` - -- [ ] 10. npm Trusted Publishing Scaffold - - **What to do**: - - Create `.github/workflows/npm-publish.yml`: - - Trigger: `workflow_dispatch` only (manual, disabled by default) - - Comment block at top explaining: - 1. First publish must be manual (`npm login && cd packages/skills-cli && npm publish --access public`) - 2. Then configure trusted publisher on npmjs.com (org: gravityforms, repo: gravityskills, workflow: npm-publish.yml) - 3. Then this workflow can be enabled - - Permissions: `id-token: write`, `contents: read` - - Jobs: - - `publish`: - - `runs-on: ubuntu-latest` - - Setup pnpm + Node 24.x (must be 24.x for npm 11.5.1+) - - `pnpm install --frozen-lockfile` - - `pnpm --filter @gravity/skills build` - - `pnpm --filter @gravity/skills test` - - `cd packages/skills-cli && npm publish --access public` - - `registry-url: 'https://registry.npmjs.org'` in setup-node (REQUIRED for trusted publishing) - - **Must NOT do**: - - Do not enable auto-trigger (no push/tag triggers) - - Do not store npm tokens - - Do not actually publish - - Do not add `--provenance` flag (automatic with trusted publishing) - - **Recommended Agent Profile**: - - **Category**: `quick` - - **Skills**: [] - - **Parallelization**: - - **Can Run In Parallel**: YES (with Tasks 8, 9) - - **Parallel Group**: Wave 3 - - **Blocks**: F1-F4 - - **Blocked By**: Task 6 - - **References**: - - **External References**: - - npm trusted publishing docs: https://docs.npmjs.com/trusted-publishers/ — OIDC setup, first-publish requirement - - Key gotcha: first publish must be manual, trusted publishing only works from second release onward - - Key gotcha: Node 24.x required for npm 11.5.1+ (trusted publishing requirement) - - Key gotcha: `registry-url` must be explicitly set in `actions/setup-node` even though it's the default - - Key gotcha: provenance attestations are automatic with trusted publishing from public repos — no `--provenance` flag needed - - **Acceptance Criteria**: - - [ ] `.github/workflows/npm-publish.yml` exists - - [ ] Only triggers on `workflow_dispatch` (no push/tag/PR triggers) - - [ ] Has `id-token: write` and `contents: read` permissions - - [ ] Has `registry-url: 'https://registry.npmjs.org'` in setup-node - - [ ] Has comment block explaining manual first-publish requirement - - [ ] Does NOT reference any npm tokens or secrets - - **QA Scenarios (MANDATORY)**: - - ``` - Scenario: npm publish workflow is manual-only - Tool: Bash - Preconditions: npm-publish.yml exists - Steps: - 1. Run `grep -c 'workflow_dispatch' .github/workflows/npm-publish.yml` → expect ≥ 1 - 2. Run `grep -c 'push:' .github/workflows/npm-publish.yml` → expect 0 - 3. Run `grep -c 'pull_request:' .github/workflows/npm-publish.yml` → expect 0 - 4. Run `grep 'id-token' .github/workflows/npm-publish.yml` → expect match containing `write` - 5. Run `grep 'registry-url' .github/workflows/npm-publish.yml` → expect match - 6. Run `grep -c 'NPM_TOKEN\|NODE_AUTH_TOKEN.*secrets' .github/workflows/npm-publish.yml` → expect 0 - Expected Result: Manual-only workflow with OIDC, no tokens - Failure Indicators: Auto-triggers present, missing OIDC, token references - Evidence: .sisyphus/evidence/task-10-npm-scaffold.txt - - Scenario: First-publish documentation exists in workflow - Tool: Bash - Preconditions: npm-publish.yml exists - Steps: - 1. Run `grep -c 'first publish' .github/workflows/npm-publish.yml` → expect ≥ 1 (case insensitive) - 2. Run `grep -c 'npm login' .github/workflows/npm-publish.yml` → expect ≥ 1 - Expected Result: Manual first-publish instructions present - Evidence: .sisyphus/evidence/task-10-npm-docs.txt - ``` - - **Commit**: YES - - Message: `ci: add disabled npm trusted publishing scaffold` - - Files: `.github/workflows/npm-publish.yml` - ---- - -## Final Verification Wave (MANDATORY — after ALL implementation tasks) - -> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing. - -- [ ] F1. **Plan Compliance Audit** — `oracle` - Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. - Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT` - -- [ ] F2. **Code Quality Review** — `unspecified-high` - Run `tsc --noEmit` + linter + `bun test`. Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names. - Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT` - -- [ ] F3. **Real Manual QA** — `unspecified-high` - Start from clean state. Run `pnpm install`, `pnpm build`, `pnpm test`, `pnpm skills:validate`, `pnpm skills:pack`. Test CLI `--help` and `install placeholder-skill`. Verify zip contents. Test each distribution channel locally. - Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT` - -- [ ] F4. **Scope Fidelity Check** — `deep` - For each task: read "What to do", read actual diff. Verify 1:1 — everything in spec was built, nothing beyond spec was built. Check "Must NOT do" compliance. Detect cross-task contamination. Flag unaccounted changes. - Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT` - ---- - -## Commit Strategy - -| Commit | Message | Files | -|--------|---------|-------| -| 1 | `chore: add runtime and pnpm workspace foundation` | `.nvmrc`, `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `.gitignore` | -| 2 | `chore: add GPL-2.0+ license and README scaffold` | `LICENSE`, `README.md` | -| 3 | `feat: add placeholder skill following Agent Skills spec` | `skills/placeholder-skill/SKILL.md` | -| 4 | `feat: add skill validation script` | `scripts/validate-skills.ts`, tests | -| 5 | `feat: add skill zip packaging script` | `scripts/pack-skills.ts`, tests | -| 6 | `feat: scaffold @gravity/skills CLI package` | `packages/skills-cli/*` | -| 7 | `feat: add Claude plugin marketplace manifest` | `.claude-plugin/marketplace.json` | -| 8 | `ci: add validation workflow` | `.github/workflows/ci.yml` | -| 9 | `ci: add release artifact workflow` | `.github/workflows/release.yml` | -| 10 | `ci: add disabled npm trusted publishing scaffold` | `.github/workflows/npm-publish.yml` | - ---- - -## Success Criteria - -### Verification Commands -```bash -pnpm install --frozen-lockfile # Expected: exits 0 -pnpm build # Expected: exits 0 -pnpm test # Expected: exits 0, all tests pass -pnpm skills:validate # Expected: exits 0, placeholder skill valid -pnpm skills:pack # Expected: exits 0, creates dist/skills/placeholder-skill.zip -unzip -t dist/skills/placeholder-skill.zip # Expected: exits 0 -node packages/skills-cli/dist/index.js --help # Expected: shows help with install command -actionlint # Expected: exits 0 on all workflow files -``` - -### Final Checklist -- [ ] All "Must Have" present -- [ ] All "Must NOT Have" absent -- [ ] All tests pass -- [ ] All three distribution channels scaffolded (GitHub Releases, npm CLI, Claude plugin) -- [ ] Supply chain hardening configured and documented From 457ab61da5f9184baf6e8b9592b02f3a5497e95c Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 07:31:35 -0700 Subject: [PATCH 14/17] refactor: move @gravity/skills CLI to gravityskills, add AGENTS.md and README (#7488) The public skills repo should contain only skills content. The CLI package now lives in gravitysuite where internal tooling belongs. Also adds AGENTS.md and public-facing README. --- .github/workflows/ci.yml | 5 +- .github/workflows/npm-publish.yml | 56 ----------- .github/workflows/release.yml | 5 +- AGENTS.md | 72 ++++++++++++++ README.md | 67 ++++++++++++- package.json | 3 +- packages/skills-cli/package.json | 36 ------- packages/skills-cli/src/__tests__/cli.test.ts | 59 ----------- packages/skills-cli/src/commands/install.ts | 98 ------------------- packages/skills-cli/src/commands/list.ts | 4 - packages/skills-cli/src/index.ts | 54 ---------- packages/skills-cli/tsconfig.json | 19 ---- pnpm-workspace.yaml | 3 - 13 files changed, 142 insertions(+), 339 deletions(-) delete mode 100644 .github/workflows/npm-publish.yml create mode 100644 AGENTS.md delete mode 100644 packages/skills-cli/package.json delete mode 100644 packages/skills-cli/src/__tests__/cli.test.ts delete mode 100644 packages/skills-cli/src/commands/install.ts delete mode 100644 packages/skills-cli/src/commands/list.ts delete mode 100644 packages/skills-cli/src/index.ts delete mode 100644 packages/skills-cli/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a8e076..4141b14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,12 +21,11 @@ jobs: node-version: '24.11.1' cache: 'pnpm' + - uses: oven-sh/setup-bun@v2 + - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build - run: pnpm build - - name: Test run: pnpm test diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml deleted file mode 100644 index ddfb760..0000000 --- a/.github/workflows/npm-publish.yml +++ /dev/null @@ -1,56 +0,0 @@ -# npm Trusted Publishing Scaffold -# -# STATUS: DISABLED — manual workflow_dispatch only -# -# HOW TO ENABLE (do this in order): -# 1. First publish must be done MANUALLY: -# npm login -# cd packages/skills-cli -# npm publish --access public -# 2. After first publish, configure Trusted Publisher on npmjs.com: -# Package: @gravity/skills -# Org: gravityforms -# Repo: gravityskills -# Workflow: npm-publish.yml -# 3. This workflow can then be triggered manually for subsequent releases. -# WHY NO NPM_TOKEN: Trusted publishing uses GitHub OIDC (id-token: write) -# to mint a short-lived publish token at runtime. No stored secrets needed. -# See: https://docs.npmjs.com/trusted-publishers/ - -name: Publish to npm - -on: - workflow_dispatch: - -permissions: - id-token: write - contents: read - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: 11.0.8 - - - uses: actions/setup-node@v4 - with: - node-version: '24.x' - registry-url: 'https://registry.npmjs.org' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build CLI - run: pnpm --filter @gravity/skills build - - - name: Test CLI - run: pnpm --filter @gravity/skills test - - - name: Publish to npm - run: npm publish --access public - working-directory: packages/skills-cli diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c87b7a9..612049c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,12 +23,11 @@ jobs: node-version: '24.11.1' cache: 'pnpm' + - uses: oven-sh/setup-bun@v2 + - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build - run: pnpm build - - name: Test run: pnpm test diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5f3cf1a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# AGENTS.md + +## Project Overview + +Public skills repository for Gravity products. pnpm 11 monorepo with supply-chain hardening that distributes agent skills via GitHub Releases (zips) and Claude Code plugin marketplace. The `@gravity/skills` CLI lives in `gravitysuite`. + +## Critical Warnings + +- **`dist/` is GENERATED** — never edit or commit. Built by `pnpm build` and `pnpm skills:pack`. +- **`.agents/`, `.specs/`, `.sisyphus/` are PRIVATE** — gitignored, never commit. These contain local-only skills, specs, and plans. +- **Zero npm dependencies in `@gravity/skills` CLI** — this is a hard constraint. The CLI lives in `gravitysuite`, not this repo. Never add runtime deps. +- **All commits on feature branches must reference `(#7488)`** in the message. + +## Commands + +```bash +# Install +pnpm install # Supply-chain-hardened (7-day minimumReleaseAge) + +# Test +pnpm test # Runs bun test + +# Skills +pnpm skills:validate # Validates skill frontmatter and directory structure +pnpm skills:pack # Packs skills into dist/skills/*.zip +``` + +## Build System + +- **Package manager**: pnpm 11.0.8 — pinned in `package.json` `packageManager` field +- **Node version**: 24.11.1 — pinned in `.nvmrc` +- **Supply chain config**: `pnpm-workspace.yaml` (authoritative source for `minimumReleaseAge`, `blockExoticSubdeps`, `trustPolicy`, `verifyDepsBeforeRun`) +- **Test runner**: `bun test` +- **Scripts use `tsx`**: `node --import tsx/esm` for TypeScript execution without compilation + +## Code Style Highlights + +- **Tabs** for indentation +- **`console.log`/`console.error` in scripts and CLI** are intentional — these are user-facing output mechanisms, not library code +- **`@ts-ignore` in test files** — acceptable only for Bun runtime module workarounds (e.g., `bun:test`) + +## Testing + +- **Framework**: Bun test (`bun test`) +- **Test location**: `scripts/__tests__/` for script tests, colocated in packages +- **Gotcha**: Tests import from `bun:test` which requires `@ts-ignore` — this is expected + +## Publishing + +Three distribution channels, all scaffolded but not yet active: + +| Channel | Workflow | Trigger | +|---------|----------|---------| +| GitHub Releases (zips) | `.github/workflows/release.yml` | `v*` tag push | +| Claude Code plugin | `.claude-plugin/marketplace.json` | Automatic from repo structure | + +- **Zip packaging**: `scripts/pack-skills.ts` → `dist/skills/` using shell `zip` via `child_process.execSync` + +## Skills Directory Structure + +Skills live in `skills/` and must follow the Agent Skills spec: + +- **Name**: lowercase, hyphens only, 1-64 chars, no consecutive hyphens +- **Directory name must match** the `name` field in SKILL.md frontmatter +- **Required file**: `SKILL.md` with YAML frontmatter (`name`, `description`) +- **Optional**: `references/` subdirectory for supporting files (included in zips) + +## Commit Convention + +Use conventional-style prefixes: `feat`, `fix`, `chore`, `docs`, `test`, `refactor` + +Format: `: (#7488)` — present tense, imperative mood, no period. diff --git a/README.md b/README.md index 51f54c2..2a944f2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ -# gravityskills -Public skills for Gravity products +# Gravity Skills + +A collection of skills for AI coding agents working with Gravity products. Skills are packaged instructions, references, and scripts that extend agent capabilities. + +Skills follow the [Agent Skills](https://agentskills.io/) format. + +## Installation + +### CLI + +```bash +npx @gravity/skills install +``` + +### GitHub Releases + +Download skill zips directly from [Releases](https://github.com/gravityforms/gravityskills/releases) and extract into your project's `.agents/skills/` directory. + +### Claude Code Plugin + +Skills are also available through the Claude Code plugin marketplace. See `.claude-plugin/marketplace.json` for the plugin manifest. + +## Available Skills + +_Coming soon._ This repository is being set up to host public skills for Gravity products. + +## Usage + +Once installed, skills are automatically available to your AI coding agent. The agent reads the skill's `SKILL.md` when working on relevant tasks. + +**Examples:** + +``` +Help me build a Gravity Forms integration +``` + +``` +Review my form field implementation +``` + +## Skill Structure + +Each skill contains: + +- `SKILL.md` — Instructions for the agent (required) +- `references/` — Supporting documentation (optional) +- `scripts/` — Helper scripts for automation (optional) + +## Contributing + +Skills must follow the [Agent Skills spec](https://agentskills.io/): + +- **Name**: lowercase, hyphens only, 1–64 characters +- **Directory name** must match the `name` field in `SKILL.md` frontmatter +- **Frontmatter**: `name` and `description` fields required + +Run validation before submitting: + +```bash +pnpm skills:validate +``` + +## License + +GPL-2.0+ diff --git a/package.json b/package.json index 0b3e4ea..517f92e 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,7 @@ "npm": "11.6.2" }, "scripts": { - "build": "pnpm -r build", - "test": "pnpm -r test", + "test": "bun test", "skills:validate": "node --import tsx/esm scripts/validate-skills.ts", "skills:pack": "node --import tsx/esm scripts/pack-skills.ts" }, diff --git a/packages/skills-cli/package.json b/packages/skills-cli/package.json deleted file mode 100644 index 798f2ae..0000000 --- a/packages/skills-cli/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@gravity/skills", - "version": "0.0.1", - "description": "Zero-dependency CLI for installing Gravity skills.", - "bin": { - "gravity-skills": "./dist/index.js" - }, - "type": "module", - "main": "./dist/index.js", - "files": [ - "dist", - "README.md" - ], - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org" - }, - "license": "GPL-2.0+", - "repository": { - "type": "git", - "url": "https://github.com/gravityforms/gravityskills" - }, - "engines": { - "node": ">=20" - }, - "scripts": { - "build": "tsc", - "test": "bun test" - }, - "dependencies": {}, - "devDependencies": { - "@types/bun": "^1.3.3", - "@types/node": "^24.10.1", - "typescript": "^5.9.3" - } -} diff --git a/packages/skills-cli/src/__tests__/cli.test.ts b/packages/skills-cli/src/__tests__/cli.test.ts deleted file mode 100644 index a90c72c..0000000 --- a/packages/skills-cli/src/__tests__/cli.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { run, usage } from "../index"; -import { installSkill } from "../commands/install"; - -function captureConsole(method: "log" | "error", callback: () => Promise | number): Promise<{ code: number; output: string[] }> { - const original = console[method]; - const output: string[] = []; - console[method] = (...args: unknown[]) => { - output.push(args.map(String).join(" ")); - }; - - return Promise.resolve(callback()).then((code) => { - console[method] = original; - return { code, output }; - }, (error) => { - console[method] = original; - throw error; - }); -} - -describe("gravity-skills CLI", () => { - test("--help output contains install and list", async () => { - const { code, output } = await captureConsole("log", () => run(["--help"])); - - expect(code).toBe(0); - expect(output.join("\n")).toContain("install"); - expect(output.join("\n")).toContain("list"); - expect(usage()).toContain("Usage"); - }); - - test("--version outputs 0.0.1", async () => { - const { code, output } = await captureConsole("log", () => run(["--version"])); - - expect(code).toBe(0); - expect(output.join("\n").trim()).toBe("0.0.1"); - }); - - test("install downloads and extracts a skill", async () => { - const calls: string[] = []; - const fakeFetch = async (url: string) => { - calls.push(String(url)); - return new Response(new Uint8Array([80, 75, 3, 4]), { status: 200 }); - }; - - const code = await installSkill(["example-skill", "--target", "/tmp/gravity-skills-test"], { - fetchImpl: fakeFetch, - unzip: (zipPath, targetDir) => { - expect(zipPath).toContain("example-skill"); - expect(targetDir).toBe("/tmp/gravity-skills-test"); - }, - log: () => undefined, - }); - - expect(code).toBe(0); - expect(calls).toEqual([ - "https://github.com/gravityforms/gravityskills/releases/latest/download/example-skill.zip", - ]); - }); -}); diff --git a/packages/skills-cli/src/commands/install.ts b/packages/skills-cli/src/commands/install.ts deleted file mode 100644 index 339066f..0000000 --- a/packages/skills-cli/src/commands/install.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { execSync } from "node:child_process"; -import { mkdir, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { basename, join, resolve } from "node:path"; - -export interface InstallOptions { - fetchImpl?: (input: string) => Promise; - unzip?: (zipPath: string, targetDir: string) => void; - log?: (message: string) => void; -} - -interface ParsedInstallArgs { - skillName: string; - targetDir: string; -} - -function parseInstallArgs(args: string[]): ParsedInstallArgs | null { - const [skillName, ...rest] = args; - - if (!skillName || skillName === "--help" || skillName === "-h") { - return null; - } - - let targetDir = process.cwd(); - - for (let index = 0; index < rest.length; index += 1) { - const arg = rest[index]; - - if (arg === "--target") { - const value = rest[index + 1]; - if (!value) { - throw new Error("Missing value for --target"); - } - targetDir = value; - index += 1; - continue; - } - - throw new Error(`Unknown install option: ${arg}`); - } - - return { skillName, targetDir: resolve(targetDir) }; -} - -function installUsage(): string { - return "Usage: gravity-skills install [--target ]"; -} - -function assertSkillName(skillName: string): void { - if (!/^[a-zA-Z0-9._-]+$/.test(skillName) || basename(skillName) !== skillName) { - throw new Error("Skill name may only contain letters, numbers, dots, underscores, and hyphens"); - } -} - -function defaultUnzip(zipPath: string, targetDir: string): void { - execSync(`unzip -oq ${JSON.stringify(zipPath)} -d ${JSON.stringify(targetDir)}`, { - stdio: "inherit", - }); -} - -export async function installSkill(args: string[], options: InstallOptions = {}): Promise { - const parsed = parseInstallArgs(args); - const log = options.log ?? console.log; - - if (!parsed) { - console.log(installUsage()); - return 0; - } - - const { skillName, targetDir } = parsed; - assertSkillName(skillName); - - const fetchImpl = options.fetchImpl ?? fetch; - const unzip = options.unzip ?? defaultUnzip; - const downloadUrl = `https://github.com/gravityforms/gravityskills/releases/latest/download/${skillName}.zip`; - const zipPath = join(tmpdir(), `gravity-skills-${skillName}-${Date.now()}.zip`); - - log(`Downloading ${skillName} from ${downloadUrl}`); - - const response = await fetchImpl(downloadUrl); - if (!response.ok) { - throw new Error(`Download failed with status ${response.status}: ${response.statusText}`); - } - - const bytes = new Uint8Array(await response.arrayBuffer()); - await writeFile(zipPath, bytes); - - try { - await mkdir(targetDir, { recursive: true }); - log(`Extracting ${skillName} to ${targetDir}`); - unzip(zipPath, targetDir); - log(`Installed ${skillName}`); - } finally { - await rm(zipPath, { force: true }); - } - - return 0; -} diff --git a/packages/skills-cli/src/commands/list.ts b/packages/skills-cli/src/commands/list.ts deleted file mode 100644 index 48cc637..0000000 --- a/packages/skills-cli/src/commands/list.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function listSkills(): number { - console.log("Visit https://github.com/gravityforms/gravityskills/releases for available skills"); - return 0; -} diff --git a/packages/skills-cli/src/index.ts b/packages/skills-cli/src/index.ts deleted file mode 100644 index 96f6988..0000000 --- a/packages/skills-cli/src/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env node - -import { installSkill } from "./commands/install.js"; -import { listSkills } from "./commands/list.js"; - -const VERSION = "0.0.1"; - -export function usage(): string { - return `Usage: gravity-skills [options] - -Commands: - install [--target ] Download and install a skill from GitHub Releases - list Show where to find available skills - -Options: - -h, --help Show this help message - -v, --version Print the CLI version -`; -} - -export async function run(argv: string[] = process.argv.slice(2)): Promise { - const [command, ...args] = argv; - - if (!command || command === "--help" || command === "-h") { - console.log(usage()); - return 0; - } - - if (command === "--version" || command === "-v") { - console.log(VERSION); - return 0; - } - - if (command === "install") { - return installSkill(args); - } - - if (command === "list") { - return listSkills(); - } - - console.error(`Unknown command: ${command}`); - console.error(usage()); - return 1; -} - -if (import.meta.url === `file://${process.argv[1]}`) { - run().then((code) => { - process.exitCode = code; - }).catch((error: unknown) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exitCode = 1; - }); -} diff --git a/packages/skills-cli/tsconfig.json b/packages/skills-cli/tsconfig.json deleted file mode 100644 index 175ca7e..0000000 --- a/packages/skills-cli/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "types": ["node", "bun"], - "rootDir": "src", - "outDir": "dist", - "declaration": true, - "sourceMap": true, - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true - }, - "include": ["src/**/*.ts"], - "exclude": ["src/__tests__/**/*.ts"] -} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4fda5bc..5c46d76 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,3 @@ -packages: - - "packages/*" - # Supply chain hardening (pnpm 11) # https://pnpm.io/supply-chain-security minimumReleaseAge: 10080 # 7 days — raised from 1d due to Mini Shai-Hulud supply chain attack (May 2026) From 9e72239dfdbd152423c47102e9f6e0d0822e7095 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 08:05:29 -0700 Subject: [PATCH 15/17] refactor: strip Node tooling, convert to pure content repo (#7488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove package.json, pnpm-workspace.yaml, pnpm-lock.yaml, .nvmrc, and TypeScript scripts/tests. Replace with shell scripts for validation and packaging. No build tooling required — just bash and zip, matching Vercel's agent-skills repo pattern. --- .github/workflows/ci.yml | 21 +- .github/workflows/release.yml | 21 +- .gitignore | 11 +- .nvmrc | 1 - AGENTS.md | 37 +-- README.md | 2 +- package.json | 19 -- pnpm-lock.yaml | 380 ---------------------- pnpm-workspace.yaml | 10 - scripts/__tests__/pack-skills.test.ts | 53 --- scripts/__tests__/validate-skills.test.ts | 94 ------ scripts/pack-skills.sh | 40 +++ scripts/pack-skills.ts | 92 ------ scripts/validate-skills.sh | 102 ++++++ scripts/validate-skills.ts | 212 ------------ 15 files changed, 154 insertions(+), 941 deletions(-) delete mode 100644 .nvmrc delete mode 100644 package.json delete mode 100644 pnpm-lock.yaml delete mode 100644 pnpm-workspace.yaml delete mode 100644 scripts/__tests__/pack-skills.test.ts delete mode 100644 scripts/__tests__/validate-skills.test.ts create mode 100755 scripts/pack-skills.sh delete mode 100644 scripts/pack-skills.ts create mode 100755 scripts/validate-skills.sh delete mode 100644 scripts/validate-skills.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4141b14..0b4c223 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,28 +12,11 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 11.0.8 - - - uses: actions/setup-node@v4 - with: - node-version: '24.11.1' - cache: 'pnpm' - - - uses: oven-sh/setup-bun@v2 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Test - run: pnpm test - - name: Validate skills - run: pnpm skills:validate + run: ./scripts/validate-skills.sh - name: Pack skills - run: pnpm skills:pack + run: ./scripts/pack-skills.sh - name: Verify zip integrity run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 612049c..b6b7076 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,28 +14,11 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - version: 11.0.8 - - - uses: actions/setup-node@v4 - with: - node-version: '24.11.1' - cache: 'pnpm' - - - uses: oven-sh/setup-bun@v2 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Test - run: pnpm test - - name: Validate skills - run: pnpm skills:validate + run: ./scripts/validate-skills.sh - name: Pack skills - run: pnpm skills:pack + run: ./scripts/pack-skills.sh - name: Create GitHub Release env: diff --git a/.gitignore b/.gitignore index daa0fa3..028d6ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,7 @@ -/.svn/ /.agents/ /.specs/ -tmp/ +/.sisyphus/ /.idea/ -codeception.yml -composer.json -composer.lock -/vendor .DS_Store .env -/node_modules -**/node_modules -local-config.json dist/ -.sisyphus/ diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 9e2934a..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -24.11.1 diff --git a/AGENTS.md b/AGENTS.md index 5f3cf1a..2ada82c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,60 +2,35 @@ ## Project Overview -Public skills repository for Gravity products. pnpm 11 monorepo with supply-chain hardening that distributes agent skills via GitHub Releases (zips) and Claude Code plugin marketplace. The `@gravity/skills` CLI lives in `gravitysuite`. +Public skills repository for Gravity products. Pure content repo — no build tooling or package manager required. Skills are distributed via GitHub Releases (zips) and Claude Code plugin marketplace. The `@gravity/skills` CLI lives in `gravitysuite`. ## Critical Warnings -- **`dist/` is GENERATED** — never edit or commit. Built by `pnpm build` and `pnpm skills:pack`. +- **`dist/` is GENERATED** — never edit or commit. Built by `scripts/pack-skills.sh`. - **`.agents/`, `.specs/`, `.sisyphus/` are PRIVATE** — gitignored, never commit. These contain local-only skills, specs, and plans. -- **Zero npm dependencies in `@gravity/skills` CLI** — this is a hard constraint. The CLI lives in `gravitysuite`, not this repo. Never add runtime deps. - **All commits on feature branches must reference `(#7488)`** in the message. ## Commands ```bash -# Install -pnpm install # Supply-chain-hardened (7-day minimumReleaseAge) +# Validate all skills +./scripts/validate-skills.sh -# Test -pnpm test # Runs bun test - -# Skills -pnpm skills:validate # Validates skill frontmatter and directory structure -pnpm skills:pack # Packs skills into dist/skills/*.zip +# Pack skills into dist/skills/*.zip +./scripts/pack-skills.sh ``` -## Build System - -- **Package manager**: pnpm 11.0.8 — pinned in `package.json` `packageManager` field -- **Node version**: 24.11.1 — pinned in `.nvmrc` -- **Supply chain config**: `pnpm-workspace.yaml` (authoritative source for `minimumReleaseAge`, `blockExoticSubdeps`, `trustPolicy`, `verifyDepsBeforeRun`) -- **Test runner**: `bun test` -- **Scripts use `tsx`**: `node --import tsx/esm` for TypeScript execution without compilation - ## Code Style Highlights - **Tabs** for indentation -- **`console.log`/`console.error` in scripts and CLI** are intentional — these are user-facing output mechanisms, not library code -- **`@ts-ignore` in test files** — acceptable only for Bun runtime module workarounds (e.g., `bun:test`) - -## Testing - -- **Framework**: Bun test (`bun test`) -- **Test location**: `scripts/__tests__/` for script tests, colocated in packages -- **Gotcha**: Tests import from `bun:test` which requires `@ts-ignore` — this is expected ## Publishing -Three distribution channels, all scaffolded but not yet active: - | Channel | Workflow | Trigger | |---------|----------|---------| | GitHub Releases (zips) | `.github/workflows/release.yml` | `v*` tag push | | Claude Code plugin | `.claude-plugin/marketplace.json` | Automatic from repo structure | -- **Zip packaging**: `scripts/pack-skills.ts` → `dist/skills/` using shell `zip` via `child_process.execSync` - ## Skills Directory Structure Skills live in `skills/` and must follow the Agent Skills spec: diff --git a/README.md b/README.md index 2a944f2..04a279e 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Skills must follow the [Agent Skills spec](https://agentskills.io/): Run validation before submitting: ```bash -pnpm skills:validate +./scripts/validate-skills.sh ``` ## License diff --git a/package.json b/package.json deleted file mode 100644 index 517f92e..0000000 --- a/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "gravityskills", - "private": true, - "packageManager": "pnpm@11.0.8", - "engines": { - "node": "24.11.1", - "npm": "11.6.2" - }, - "scripts": { - "test": "bun test", - "skills:validate": "node --import tsx/esm scripts/validate-skills.ts", - "skills:pack": "node --import tsx/esm scripts/pack-skills.ts" - }, - "devDependencies": { - "@types/bun": "^1.3.13", - "@types/node": "^25.7.0", - "tsx": "^4.21.0" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 1f07182..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,380 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@types/bun': - specifier: ^1.3.13 - version: 1.3.13 - '@types/node': - specifier: ^25.7.0 - version: 25.7.0 - tsx: - specifier: ^4.21.0 - version: 4.21.0 - - packages/skills-cli: - devDependencies: - '@types/bun': - specifier: ^1.3.3 - version: 1.3.13 - '@types/node': - specifier: ^24.10.1 - version: 24.12.4 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - -packages: - - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@types/bun@1.3.13': - resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==} - - '@types/node@24.12.4': - resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} - - '@types/node@25.7.0': - resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} - - bun-types@1.3.13: - resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==} - - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-tsconfig@4.14.0: - resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - - undici-types@7.21.0: - resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} - -snapshots: - - '@esbuild/aix-ppc64@0.27.7': - optional: true - - '@esbuild/android-arm64@0.27.7': - optional: true - - '@esbuild/android-arm@0.27.7': - optional: true - - '@esbuild/android-x64@0.27.7': - optional: true - - '@esbuild/darwin-arm64@0.27.7': - optional: true - - '@esbuild/darwin-x64@0.27.7': - optional: true - - '@esbuild/freebsd-arm64@0.27.7': - optional: true - - '@esbuild/freebsd-x64@0.27.7': - optional: true - - '@esbuild/linux-arm64@0.27.7': - optional: true - - '@esbuild/linux-arm@0.27.7': - optional: true - - '@esbuild/linux-ia32@0.27.7': - optional: true - - '@esbuild/linux-loong64@0.27.7': - optional: true - - '@esbuild/linux-mips64el@0.27.7': - optional: true - - '@esbuild/linux-ppc64@0.27.7': - optional: true - - '@esbuild/linux-riscv64@0.27.7': - optional: true - - '@esbuild/linux-s390x@0.27.7': - optional: true - - '@esbuild/linux-x64@0.27.7': - optional: true - - '@esbuild/netbsd-arm64@0.27.7': - optional: true - - '@esbuild/netbsd-x64@0.27.7': - optional: true - - '@esbuild/openbsd-arm64@0.27.7': - optional: true - - '@esbuild/openbsd-x64@0.27.7': - optional: true - - '@esbuild/openharmony-arm64@0.27.7': - optional: true - - '@esbuild/sunos-x64@0.27.7': - optional: true - - '@esbuild/win32-arm64@0.27.7': - optional: true - - '@esbuild/win32-ia32@0.27.7': - optional: true - - '@esbuild/win32-x64@0.27.7': - optional: true - - '@types/bun@1.3.13': - dependencies: - bun-types: 1.3.13 - - '@types/node@24.12.4': - dependencies: - undici-types: 7.16.0 - - '@types/node@25.7.0': - dependencies: - undici-types: 7.21.0 - - bun-types@1.3.13: - dependencies: - '@types/node': 25.7.0 - - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.14.0: - dependencies: - resolve-pkg-maps: 1.0.0 - - resolve-pkg-maps@1.0.0: {} - - tsx@4.21.0: - dependencies: - esbuild: 0.27.7 - get-tsconfig: 4.14.0 - optionalDependencies: - fsevents: 2.3.3 - - typescript@5.9.3: {} - - undici-types@7.16.0: {} - - undici-types@7.21.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 5c46d76..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Supply chain hardening (pnpm 11) -# https://pnpm.io/supply-chain-security -minimumReleaseAge: 10080 # 7 days — raised from 1d due to Mini Shai-Hulud supply chain attack (May 2026) -blockExoticSubdeps: true # Block git/tarball transitive dependencies -trustPolicy: no-downgrade # Reject packages with reduced trust level vs prior releases -verifyDepsBeforeRun: install # Verify lockfile integrity before any pnpm run -allowBuilds: - esbuild: false -ignoredBuiltDependencies: - - esbuild diff --git a/scripts/__tests__/pack-skills.test.ts b/scripts/__tests__/pack-skills.test.ts deleted file mode 100644 index 771c8e5..0000000 --- a/scripts/__tests__/pack-skills.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/// - -import { execFileSync } from 'node:child_process'; -import { existsSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; -import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; - -const repoRoot = join(import.meta.dirname, '..', '..'); -const distDir = join(repoRoot, 'dist'); -const zipPath = join(distDir, 'skills', 'placeholder-skill.zip'); - -function zipEntries(): string[] { - return execFileSync('zipinfo', ['-1', zipPath], { - cwd: repoRoot, - encoding: 'utf8', - }) - .trim() - .split('\n') - .filter(Boolean); -} - -describe('pack skills', () => { - beforeAll(() => { - rmSync(distDir, { recursive: true, force: true }); - execFileSync('pnpm', ['skills:pack'], { cwd: repoRoot, stdio: 'pipe' }); - }); - - afterAll(() => { - rmSync(distDir, { recursive: true, force: true }); - }); - - test('creates the placeholder skill zip', () => { - expect(existsSync(zipPath)).toBe(true); - }); - - test('contains SKILL.md under the skill directory root', () => { - expect(zipEntries()).toContain('placeholder-skill/SKILL.md'); - }); - - test('keeps the skill directory as the zip root', () => { - const entries = zipEntries(); - - expect(entries).not.toContain('SKILL.md'); - expect(entries.every((entry) => entry.startsWith('placeholder-skill/'))).toBe(true); - }); - - test('excludes ignored files and directories', () => { - const entries = zipEntries(); - - expect(entries.some((entry) => entry.includes('.DS_Store'))).toBe(false); - expect(entries.some((entry) => entry.includes('node_modules/'))).toBe(false); - }); -}); diff --git a/scripts/__tests__/validate-skills.test.ts b/scripts/__tests__/validate-skills.test.ts deleted file mode 100644 index 916fb4f..0000000 --- a/scripts/__tests__/validate-skills.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -// @ts-ignore Bun provides this module at test runtime. -import { afterEach, describe, expect, test } from 'bun:test'; - -const { validateSkills } = require('../validate-skills'); - -const tempRoots: string[] = []; - -function createRepo(): string { - const repoRoot = mkdtempSync(join(tmpdir(), 'gravityskills-validate-')); - tempRoots.push(repoRoot); - mkdirSync(join(repoRoot, 'skills'), { recursive: true }); - - return repoRoot; -} - -function writeSkill(repoRoot: string, directoryName: string, frontmatter: string): void { - const skillDir = join(repoRoot, 'skills', directoryName); - mkdirSync(skillDir, { recursive: true }); - writeFileSync(join(skillDir, 'SKILL.md'), `---\n${frontmatter}---\n\n# ${directoryName}\n`); -} - -afterEach(() => { - for (const root of tempRoots.splice(0)) { - rmSync(root, { recursive: true, force: true }); - } -}); - -describe('validateSkills', () => { - test('passes for valid skills', () => { - const repoRoot = createRepo(); - writeSkill(repoRoot, 'placeholder-skill', 'name: placeholder-skill\ndescription: A placeholder skill\n'); - - expect(validateSkills(repoRoot)).toEqual({ - errors: [], - skillCount: 1, - }); - }); - - test('catches missing name', () => { - const repoRoot = createRepo(); - writeSkill(repoRoot, 'missing-name', 'description: A valid description\n'); - - expect(validateSkills(repoRoot).errors).toContain('skills/missing-name/SKILL.md: frontmatter.name is required'); - }); - - test('catches invalid name format', () => { - const repoRoot = createRepo(); - writeSkill(repoRoot, 'invalid-format', 'name: Invalid--Name\ndescription: A valid description\n'); - - expect(validateSkills(repoRoot).errors).toContain( - 'skills/invalid-format/SKILL.md: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens', - ); - }); - - test('catches name and directory mismatch', () => { - const repoRoot = createRepo(); - writeSkill(repoRoot, 'directory-name', 'name: frontmatter-name\ndescription: A valid description\n'); - - expect(validateSkills(repoRoot).errors).toContain( - 'skills/directory-name/SKILL.md: frontmatter.name must match parent directory name "directory-name"', - ); - }); - - test('catches missing description', () => { - const repoRoot = createRepo(); - writeSkill(repoRoot, 'missing-description', 'name: missing-description\n'); - - expect(validateSkills(repoRoot).errors).toContain( - 'skills/missing-description/SKILL.md: frontmatter.description is required', - ); - }); - - test('catches overlong description', () => { - const repoRoot = createRepo(); - writeSkill(repoRoot, 'long-description', `name: long-description\ndescription: ${'a'.repeat(1025)}\n`); - - expect(validateSkills(repoRoot).errors).toContain( - 'skills/long-description/SKILL.md: frontmatter.description must be 1-1024 characters', - ); - }); - - test('catches duplicate names', () => { - const repoRoot = createRepo(); - writeSkill(repoRoot, 'duplicate-one', 'name: duplicate-skill\ndescription: A valid description\n'); - writeSkill(repoRoot, 'duplicate-two', 'name: duplicate-skill\ndescription: Another valid description\n'); - - expect(validateSkills(repoRoot).errors).toContain( - 'skills/duplicate-one/SKILL.md, skills/duplicate-two/SKILL.md: duplicate skill name "duplicate-skill"', - ); - }); -}); diff --git a/scripts/pack-skills.sh b/scripts/pack-skills.sh new file mode 100755 index 0000000..87f8766 --- /dev/null +++ b/scripts/pack-skills.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Packs each skill directory into dist/skills/{name}.zip +# Excludes: node_modules, .git, .DS_Store, .env, __pycache__, *.tgz + +set -euo pipefail + +SKILLS_DIR="${1:-skills}" +OUTPUT_DIR="dist/skills" + +if [[ ! -d "$SKILLS_DIR" ]]; then + echo "Error: skills directory does not exist: $SKILLS_DIR" >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" + +for skill_dir in "$SKILLS_DIR"/*/; do + [[ -d "$skill_dir" ]] || continue + + skill_name=$(basename "$skill_dir") + zip_path="$OUTPUT_DIR/$skill_name.zip" + + rm -f "$zip_path" + + (cd "$SKILLS_DIR" && zip -q -r "../$zip_path" "$skill_name" \ + -x "**/node_modules/*" \ + -x "**/.git/*" \ + -x "**/.DS_Store" \ + -x "**/.env" \ + -x "**/__pycache__/*" \ + -x "**.tgz") + + size=$(wc -c < "$zip_path" | tr -d ' ') + if [[ $size -lt 1024 ]]; then + echo "Created $zip_path ($size B)" + else + kb=$(echo "scale=1; $size / 1024" | bc) + echo "Created $zip_path ($kb KB)" + fi +done diff --git a/scripts/pack-skills.ts b/scripts/pack-skills.ts deleted file mode 100644 index 91afa2c..0000000 --- a/scripts/pack-skills.ts +++ /dev/null @@ -1,92 +0,0 @@ -{ -const { execFileSync } = require('node:child_process'); -const { mkdirSync, readdirSync, rmSync, statSync } = require('node:fs'); -const { join, relative } = require('node:path'); - -const repoRoot = process.cwd(); -const skillsDir = join(repoRoot, 'skills'); -const outputDir = join(repoRoot, 'dist', 'skills'); - -const excludedNames = new Set(['node_modules', '.git', '.DS_Store', '.env', '__pycache__']); - -function getSkillDirectories(): string[] { - return readdirSync(skillsDir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .sort(); -} - -function shouldExclude(path: string): boolean { - const parts = path.split('/'); - const name = parts[parts.length - 1] ?? ''; - - return parts.some((part) => excludedNames.has(part)) || name.endsWith('.tgz'); -} - -function getIncludedFiles(skillName: string): string[] { - const skillDir = join(skillsDir, skillName); - const files: string[] = []; - - function walk(directory: string): void { - for (const entry of readdirSync(directory, { withFileTypes: true })) { - const absolutePath = join(directory, entry.name); - const archivePath = relative(skillsDir, absolutePath).split('/').join('/'); - - if (shouldExclude(archivePath)) { - continue; - } - - if (entry.isDirectory()) { - walk(absolutePath); - } else if (entry.isFile()) { - files.push(archivePath); - } - } - } - - walk(skillDir); - - return files.sort(); -} - -function formatBytes(bytes: number): string { - if (bytes < 1024) { - return `${bytes} B`; - } - - return `${(bytes / 1024).toFixed(1)} KB`; -} - -function packSkills(): void { - mkdirSync(outputDir, { recursive: true }); - - const skills = getSkillDirectories(); - - for (const skillName of skills) { - const zipPath = join(outputDir, `${skillName}.zip`); - const includedFiles = getIncludedFiles(skillName); - - rmSync(zipPath, { force: true }); - - if (includedFiles.length === 0) { - throw new Error(`No files found to package for ${skillName}`); - } - - execFileSync('zip', ['-q', '-r', zipPath, ...includedFiles], { - cwd: skillsDir, - stdio: 'pipe', - }); - - const size = statSync(zipPath).size; - console.log(`Created ${relative(repoRoot, zipPath)} (${formatBytes(size)})`); - } -} - -try { - packSkills(); -} catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`Failed to pack skills: ${message}`); - process.exitCode = 1; -} -} diff --git a/scripts/validate-skills.sh b/scripts/validate-skills.sh new file mode 100755 index 0000000..f4eb781 --- /dev/null +++ b/scripts/validate-skills.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Validates all skills in the skills/ directory. +# +# Checks: +# - Each subdirectory has a SKILL.md with YAML frontmatter +# - name: lowercase, hyphens, 1-64 chars, no consecutive/leading/trailing hyphens +# - name matches parent directory name +# - description: required, ≤1024 chars +# - No duplicate skill names + +set -euo pipefail + +SKILLS_DIR="${1:-skills}" +errors=() +names=() +name_paths=() +count=0 + +if [[ ! -d "$SKILLS_DIR" ]]; then + echo "Error: skills directory does not exist: $SKILLS_DIR" >&2 + exit 1 +fi + +for skill_dir in "$SKILLS_DIR"/*/; do + [[ -d "$skill_dir" ]] || continue + + dir_name=$(basename "$skill_dir") + skill_file="$skill_dir/SKILL.md" + count=$((count + 1)) + + if [[ ! -f "$skill_file" ]]; then + errors+=("$skill_file: SKILL.md is required") + continue + fi + + content=$(cat "$skill_file") + + # Check frontmatter delimiters + if [[ "$content" != ---* ]]; then + errors+=("$skill_file: missing YAML frontmatter opening delimiter") + continue + fi + + # Extract frontmatter (between first and second ---) + frontmatter=$(echo "$content" | sed -n '2,/^---$/{ /^---$/d; p; }') + + if [[ -z "$frontmatter" ]]; then + errors+=("$skill_file: missing YAML frontmatter closing delimiter") + continue + fi + + # Extract name (handles quoted and unquoted values) + name=$(echo "$frontmatter" | grep -E '^name:' | head -1 | sed 's/^name:[[:space:]]*//' | sed 's/^["'\'']\(.*\)["'\'']/\1/') + + # Extract description + description=$(echo "$frontmatter" | grep -E '^description:' | head -1 | sed 's/^description:[[:space:]]*//' | sed 's/^["'\'']\(.*\)["'\'']/\1/') + + # Validate name + if [[ -z "$name" ]]; then + errors+=("$skill_file: frontmatter.name is required") + else + if ! echo "$name" | grep -qE '^[a-z]([a-z-]{0,62}[a-z])?$'; then + errors+=("$skill_file: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens") + elif echo "$name" | grep -q '\-\-'; then + errors+=("$skill_file: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens") + fi + + if [[ "$name" != "$dir_name" ]]; then + errors+=("$skill_file: frontmatter.name must match parent directory name \"$dir_name\"") + fi + + # Track for duplicate detection + names+=("$name") + name_paths+=("$skill_file") + fi + + # Validate description + if [[ -z "$description" ]]; then + errors+=("$skill_file: frontmatter.description is required") + elif [[ ${#description} -gt 1024 ]]; then + errors+=("$skill_file: frontmatter.description must be 1-1024 characters") + fi +done + +# Check for duplicate names +for ((i = 0; i < ${#names[@]}; i++)); do + for ((j = i + 1; j < ${#names[@]}; j++)); do + if [[ "${names[$i]}" == "${names[$j]}" ]]; then + errors+=("${name_paths[$i]}, ${name_paths[$j]}: duplicate skill name \"${names[$i]}\"") + fi + done +done + +if [[ ${#errors[@]} -gt 0 ]]; then + echo "Skill validation failed with ${#errors[@]} error(s):" >&2 + for error in "${errors[@]}"; do + echo "- $error" >&2 + done + exit 1 +fi + +echo "Validated $count skill$([ "$count" -eq 1 ] && echo '' || echo 's')." diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts deleted file mode 100644 index 58a80f9..0000000 --- a/scripts/validate-skills.ts +++ /dev/null @@ -1,212 +0,0 @@ -const fs = require('node:fs'); -const path = require('node:path'); - -interface SkillFrontmatter { - name?: unknown; - description?: unknown; - [key: string]: unknown; -} - -interface ValidationResult { - errors: string[]; - skillCount: number; -} - -const skillNamePattern = /^(?!.*--)[a-z](?:[a-z-]{0,62}[a-z])?$/; -const defaultRepoRoot = process.cwd(); - -function formatPath(filePath: string, repoRoot: string): string { - const relativePath = path.relative(repoRoot, filePath); - - return relativePath === '' ? filePath : relativePath.split('\\').join('/'); -} - -function parseScalar(value: string): unknown { - const trimmed = value.trim(); - - if (trimmed === '') { - return ''; - } - - const quote = trimmed[0]; - if ((quote === '"' || quote === "'") && trimmed.endsWith(quote)) { - return trimmed.slice(1, -1); - } - - if (trimmed === 'true') { - return true; - } - - if (trimmed === 'false') { - return false; - } - - return trimmed; -} - -function parseFrontmatter(markdown: string): SkillFrontmatter { - const normalized = markdown.replace(/^\uFEFF/, ''); - - if (!normalized.startsWith('---\n') && normalized.trim() !== '---') { - throw new Error('missing YAML frontmatter opening delimiter'); - } - - const closingDelimiter = normalized.indexOf('\n---', 4); - if (closingDelimiter === -1) { - throw new Error('missing YAML frontmatter closing delimiter'); - } - - const frontmatter = normalized.slice(4, closingDelimiter); - const result: SkillFrontmatter = {}; - const parents: Array> = [result]; - const indents = [0]; - - for (const rawLine of frontmatter.split('\n')) { - if (rawLine.trim() === '' || rawLine.trimStart().startsWith('#')) { - continue; - } - - const match = rawLine.match(/^(\s*)([^:#][^:]*):(?:\s*(.*))?$/); - if (!match) { - continue; - } - - const indent = match[1].length; - const key = match[2].trim(); - const value = match[3] ?? ''; - - while (indents.length > 1 && indent <= indents[indents.length - 1]) { - parents.pop(); - indents.pop(); - } - - const parent = parents[parents.length - 1]; - if (value.trim() === '') { - const child: Record = {}; - parent[key] = child; - parents.push(child); - indents.push(indent); - continue; - } - - parent[key] = parseScalar(value); - } - - return result; -} - -function validateName(filePath: string, repoRoot: string, directoryName: string, name: unknown): string[] { - const location = formatPath(filePath, repoRoot); - const errors: string[] = []; - - if (typeof name !== 'string' || name.trim() === '') { - errors.push(`${location}: frontmatter.name is required`); - return errors; - } - - if (!skillNamePattern.test(name)) { - errors.push(`${location}: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens`); - } - - if (name !== directoryName) { - errors.push(`${location}: frontmatter.name must match parent directory name "${directoryName}"`); - } - - return errors; -} - -function validateDescription(filePath: string, repoRoot: string, description: unknown): string[] { - const location = formatPath(filePath, repoRoot); - - if (typeof description !== 'string' || description.trim() === '') { - return [`${location}: frontmatter.description is required`]; - } - - if (description.length > 1024) { - return [`${location}: frontmatter.description must be 1-1024 characters`]; - } - - return []; -} - -function validateSkills(repoRoot = defaultRepoRoot): ValidationResult { - const skillsDir = path.join(repoRoot, 'skills'); - const errors: string[] = []; - const names = new Map(); - - if (!fs.existsSync(skillsDir)) { - return { - errors: [`${formatPath(skillsDir, repoRoot)}: skills directory does not exist`], - skillCount: 0, - }; - } - - const skillDirectories = fs.readdirSync(skillsDir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .sort(); - - for (const directoryName of skillDirectories) { - const filePath = path.join(skillsDir, directoryName, 'SKILL.md'); - const location = formatPath(filePath, repoRoot); - - if (!fs.existsSync(filePath)) { - errors.push(`${location}: SKILL.md is required`); - continue; - } - - let frontmatter: SkillFrontmatter; - try { - frontmatter = parseFrontmatter(fs.readFileSync(filePath, 'utf8')); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - errors.push(`${location}: ${message}`); - continue; - } - - errors.push(...validateName(filePath, repoRoot, directoryName, frontmatter.name)); - errors.push(...validateDescription(filePath, repoRoot, frontmatter.description)); - - if (typeof frontmatter.name === 'string' && frontmatter.name.trim() !== '') { - const paths = names.get(frontmatter.name) ?? []; - paths.push(location); - names.set(frontmatter.name, paths); - } - } - - for (const [name, paths] of names) { - if (paths.length > 1) { - errors.push(`${paths.join(', ')}: duplicate skill name "${name}"`); - } - } - - return { - errors, - skillCount: skillDirectories.length, - }; -} - -function runValidation(repoRoot = defaultRepoRoot): number { - const result = validateSkills(repoRoot); - - if (result.errors.length > 0) { - console.error(`Skill validation failed with ${result.errors.length} error(s):`); - for (const error of result.errors) { - console.error(`- ${error}`); - } - return 1; - } - - console.log(`Validated ${result.skillCount} skill${result.skillCount === 1 ? '' : 's'}.`); - return 0; -} - -if (path.basename(process.argv[1] ?? '') === 'validate-skills.ts') { - process.exitCode = runValidation(); -} - -module.exports = { - parseFrontmatter, - runValidation, - validateSkills, -}; From e3d520a76e52517c9e9be5ad56c44373cde7c15c Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 12:26:42 -0700 Subject: [PATCH 16/17] chore: remove Claude Code plugin marketplace (#7488) Plugin marketplace is for richer packages (MCP + skills + hooks), not pure skill distribution. A proper GF Claude plugin will live in its own repo when the abilities API work lands. --- .claude-plugin/marketplace.json | 22 ---------------------- AGENTS.md | 3 +-- README.md | 4 ---- 3 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 .claude-plugin/marketplace.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json deleted file mode 100644 index 1ec3f0d..0000000 --- a/.claude-plugin/marketplace.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "gravity-skills", - "owner": { - "name": "Gravity Forms", - "email": "support@gravityforms.com" - }, - "metadata": { - "description": "Official skills for Gravity Forms, Gravity SMTP, and other Gravity products", - "version": "1.0.0" - }, - "plugins": [ - { - "name": "gravity-skills", - "description": "Skills for working with Gravity products", - "source": "./", - "strict": false, - "skills": [ - "./skills/placeholder-skill" - ] - } - ] -} diff --git a/AGENTS.md b/AGENTS.md index 2ada82c..2495741 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Overview -Public skills repository for Gravity products. Pure content repo — no build tooling or package manager required. Skills are distributed via GitHub Releases (zips) and Claude Code plugin marketplace. The `@gravity/skills` CLI lives in `gravitysuite`. +Public skills repository for Gravity products. Pure content repo — no build tooling or package manager required. Skills are distributed via GitHub Releases (zips). The `@gravity/skills` CLI lives in the Gravity Monorepo. ## Critical Warnings @@ -29,7 +29,6 @@ Public skills repository for Gravity products. Pure content repo — no build to | Channel | Workflow | Trigger | |---------|----------|---------| | GitHub Releases (zips) | `.github/workflows/release.yml` | `v*` tag push | -| Claude Code plugin | `.claude-plugin/marketplace.json` | Automatic from repo structure | ## Skills Directory Structure diff --git a/README.md b/README.md index 04a279e..51b9966 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,6 @@ npx @gravity/skills install Download skill zips directly from [Releases](https://github.com/gravityforms/gravityskills/releases) and extract into your project's `.agents/skills/` directory. -### Claude Code Plugin - -Skills are also available through the Claude Code plugin marketplace. See `.claude-plugin/marketplace.json` for the plugin manifest. - ## Available Skills _Coming soon._ This repository is being set up to host public skills for Gravity products. From 22d8777903fe8c9d286800941fb9f841e91b6b30 Mon Sep 17 00:00:00 2001 From: Samuel Estok Date: Wed, 13 May 2026 12:35:16 -0700 Subject: [PATCH 17/17] fix: address Copilot review feedback (#7488) - Replace pnpm refs with shell script paths in placeholder SKILL.md - Fix **.tgz glob pattern in pack-skills.sh - Replace bc dependency with pure bash arithmetic - Fix frontmatter closing delimiter detection in validate-skills.sh - Allow digits in skill names per Agent Skills spec - Document single-line description restriction - Tighten release tag pattern to v[0-9]* - Add zip integrity verification to release workflow - Remove hardcoded issue ref from commit convention - Gate CLI install behind coming-soon note in README --- .github/workflows/release.yml | 8 +++++++- AGENTS.md | 3 +-- README.md | 4 +++- scripts/pack-skills.sh | 8 +++++--- scripts/validate-skills.sh | 16 +++++++++------- skills/placeholder-skill/SKILL.md | 5 ++--- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6b7076..49dcf41 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - 'v*' + - 'v[0-9]*' permissions: contents: write @@ -20,6 +20,12 @@ jobs: - name: Pack skills run: ./scripts/pack-skills.sh + - name: Verify zip integrity + run: | + for zip in dist/skills/*.zip; do + unzip -t "$zip" + done + - name: Create GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 2495741..14ccf68 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,6 @@ Public skills repository for Gravity products. Pure content repo — no build to - **`dist/` is GENERATED** — never edit or commit. Built by `scripts/pack-skills.sh`. - **`.agents/`, `.specs/`, `.sisyphus/` are PRIVATE** — gitignored, never commit. These contain local-only skills, specs, and plans. -- **All commits on feature branches must reference `(#7488)`** in the message. ## Commands @@ -43,4 +42,4 @@ Skills live in `skills/` and must follow the Agent Skills spec: Use conventional-style prefixes: `feat`, `fix`, `chore`, `docs`, `test`, `refactor` -Format: `: (#7488)` — present tense, imperative mood, no period. +Format: `: ` — present tense, imperative mood, no period. diff --git a/README.md b/README.md index 51b9966..433c28a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ Skills follow the [Agent Skills](https://agentskills.io/) format. ## Installation -### CLI +### CLI (coming soon) + +> The `@gravity/skills` CLI is not yet published. Once available: ```bash npx @gravity/skills install diff --git a/scripts/pack-skills.sh b/scripts/pack-skills.sh index 87f8766..9de0838 100755 --- a/scripts/pack-skills.sh +++ b/scripts/pack-skills.sh @@ -28,13 +28,15 @@ for skill_dir in "$SKILLS_DIR"/*/; do -x "**/.DS_Store" \ -x "**/.env" \ -x "**/__pycache__/*" \ - -x "**.tgz") + -x "*.tgz" \ + -x "**/*.tgz") size=$(wc -c < "$zip_path" | tr -d ' ') if [[ $size -lt 1024 ]]; then echo "Created $zip_path ($size B)" else - kb=$(echo "scale=1; $size / 1024" | bc) - echo "Created $zip_path ($kb KB)" + kb=$((size / 1024)) + remainder=$(( (size % 1024) * 10 / 1024 )) + echo "Created $zip_path ($kb.$remainder KB)" fi done diff --git a/scripts/validate-skills.sh b/scripts/validate-skills.sh index f4eb781..c50c5e7 100755 --- a/scripts/validate-skills.sh +++ b/scripts/validate-skills.sh @@ -42,27 +42,29 @@ for skill_dir in "$SKILLS_DIR"/*/; do fi # Extract frontmatter (between first and second ---) - frontmatter=$(echo "$content" | sed -n '2,/^---$/{ /^---$/d; p; }') - - if [[ -z "$frontmatter" ]]; then + # First verify closing delimiter exists + closing_line=$(echo "$content" | tail -n +2 | grep -n '^---$' | head -1 | cut -d: -f1 || true) + if [[ -z "$closing_line" ]]; then errors+=("$skill_file: missing YAML frontmatter closing delimiter") continue fi + frontmatter=$(echo "$content" | sed -n "2,$((closing_line))p" | sed '$d') + # Extract name (handles quoted and unquoted values) name=$(echo "$frontmatter" | grep -E '^name:' | head -1 | sed 's/^name:[[:space:]]*//' | sed 's/^["'\'']\(.*\)["'\'']/\1/') - # Extract description + # Extract description (single-line values only; multi-line YAML scalars are not supported) description=$(echo "$frontmatter" | grep -E '^description:' | head -1 | sed 's/^description:[[:space:]]*//' | sed 's/^["'\'']\(.*\)["'\'']/\1/') # Validate name if [[ -z "$name" ]]; then errors+=("$skill_file: frontmatter.name is required") else - if ! echo "$name" | grep -qE '^[a-z]([a-z-]{0,62}[a-z])?$'; then - errors+=("$skill_file: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens") + if ! echo "$name" | grep -qE '^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$'; then + errors+=("$skill_file: frontmatter.name must be lowercase alphanumeric and hyphens, 1-64 chars, no leading/trailing/consecutive hyphens") elif echo "$name" | grep -q '\-\-'; then - errors+=("$skill_file: frontmatter.name must be lowercase hyphenated text, 1-64 chars, with no leading/trailing/consecutive hyphens") + errors+=("$skill_file: frontmatter.name must not contain consecutive hyphens") fi if [[ "$name" != "$dir_name" ]]; then diff --git a/skills/placeholder-skill/SKILL.md b/skills/placeholder-skill/SKILL.md index 6bb610d..f8d97fb 100644 --- a/skills/placeholder-skill/SKILL.md +++ b/skills/placeholder-skill/SKILL.md @@ -13,10 +13,9 @@ metadata: > **This is a test skill.** It exists to validate the Gravity Skills repository framework and distribution pipeline. It does not provide any real functionality. This skill is used to verify: -- Skill validation (`pnpm skills:validate`) correctly parses SKILL.md frontmatter -- Zip packaging (`pnpm skills:pack`) correctly bundles skill directories +- Skill validation (`./scripts/validate-skills.sh`) correctly parses SKILL.md frontmatter +- Zip packaging (`./scripts/pack-skills.sh`) correctly bundles skill directories - GitHub Releases distribution works end-to-end -- Claude Code plugin marketplace installation works ## When to use