Skip to content
Back to skills

Wp Contributing

ASecurity

Documents how to change the claude-wp-builder plugin itself — the four-layer architecture (commands, agents, skills, starter-theme) and what may call what, why checks in tests/checks/ are grep gates over prose, the frontmatter contract per layer, the two i18n systems, opening a PR, and what a release rests on. Use when editing this repository's own commands/, agents/, skills/, starter-theme/, bin/ or tests/, when writing a check, opening a PR, cutting a release or editing CHANGELOG.md, BACKLO...

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 2, 2026
toolsgophpshellbashnodetestinggitdatabase

Works with

  • claude code
  • cli

Security analysis

A100/100

Scanned October 7, 2026

npx -y skills add yojahny55/claude-wp-builder --skill wp-contributing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Wp Contributing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Wp Contributing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yojahny55-wp-contributing/badge)](https://www.skillsdirectory.com/skills/yojahny55-wp-contributing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: wp-contributing
description: Documents how to change the claude-wp-builder plugin itself — the four-layer architecture (commands, agents, skills, starter-theme) and what may call what, why checks in tests/checks/ are grep gates over prose, the frontmatter contract per layer, the two i18n systems, opening a PR, and what a release rests on. Use when editing this repository's own commands/, agents/, skills/, starter-theme/, bin/ or tests/, when writing a check, opening a PR, cutting a release or editing CHANGELOG.md, BACKLOG.md or the .claude-plugin/ version files, or when running /wp-contribute. Not for building a WordPress site with the plugin, or for testing some other WordPress plugin.
user-invocable: false
---

# Contributing to Claude WP Builder

`CONTRIBUTING.md` is the front door: how to fork, where files go, the PR template, the code
of conduct. Read it first. This skill is the part that is not written down anywhere else —
the rules you only learn by breaking them.

**The one sentence that explains this repo:** it is a plugin made almost entirely of prose,
so nothing fails at runtime when the prose goes wrong. A contract that drifts produces a
worse site, silently, three commands later. Every convention below exists because that
already happened.

---

## Part 1 — Contributors

### The four layers, and what may call what

```
commands/*.md     user-facing slash commands — orchestration, argument parsing, step order
   ↓ dispatches
agents/*.md       specialized subagents (wp-template, wp-css, wp-acf, wp-audit-*, …)
   ↓ reads
skills/*/SKILL.md knowledge libraries — methodology, never actions
   ↓ produces
starter-theme/    PHP scaffolds copied into the user's project
```

Calls go **down** only. The rules that follow from that:

- **A command must never reimplement a builder.** `/wp-yolo` is the model: it normalizes the
  demo once, then drives `/wp-settings`, `/wp-cpt`, `/wp-header`, `/wp-footer`, `/wp-section`
  and `/wp-seed` in dependency order. It owns the *order*, never the *work*. When you need a
  command to do something a builder already does, dispatch that builder — a second
  implementation is a second thing to keep correct, and it will diverge.
- **Skills inform; they never act.** A skill that tells an agent to run WP-CLI belongs in an
  agent. `user-invocable: false` is not decoration — it is the whole contract.
- **Every agent's first mandatory step reads the project's `.claude/CLAUDE.md`** — the file
  `/wp-init` generates in the *user's* WordPress project, which is a different file from this
  repository's own `CLAUDE.md`. It carries the function prefix, theme slug, languages,
  template and i18n strategy. `prefix_` in any agent or skill doc is a placeholder, never a
  literal.
- **Plugin-relative paths in commands, agents and skills are always `${CLAUDE_PLUGIN_ROOT}/…`**,
  never relative. A relative path resolves against the user's project, not the plugin.

### Checks are grep gates over prose

There is no runner and no framework. Each check under `tests/checks/` is a standalone bash
script that prints `PASS` or exits non-zero. Run them from the repo root:

```bash
bash tests/checks/wp-yolo-gate.sh               # one check
```

The whole set runs with the loop under "Before you open a PR" — never a bare `for` loop,
whose status is only its last check's. A check that prints `PASS` with a `SKIP` line beside it
did not run its real half: the browser checks skip without `playwright-core` and a Chromium or
Firefox build, and run for real with
`PLAYWRIGHT_CORE="$(npm root -g)/@playwright/test/node_modules/playwright-core"` set.

Because the "code" is instructions to a model, a check asserts that **the contract wording is
still present** in the command, agent or skill that owns it. That sounds weak and is not: the
wording IS the behavior. If a check would pass with the sentence deleted, it protects nothing.

**Writing one.** Name the defect in a comment at the top — what shipped, what it cost — then
assert the narrowest thing that would have caught it:

```bash
#!/usr/bin/env bash
# /wp-section --hybrid: /wp-init and docs/cinematic-mode.md both send users to this flag,
# so commands/wp-section.md must actually define it — and define it as the trailing-flex
# overlay the cinematic starter's front-page.php renders, not a standalone field group.
set -euo pipefail
cd "$(dirname "$0")/../.."
fail() { echo "FAIL: $*"; exit 1; }

grep -Fq -- '--hybrid' commands/wp-section.md || fail "wp-section.md does not parse --hybrid"
grep -Fq "have_rows('trailing_sections')" starter-theme/__cinematic__/front-page.php \
  || fail "cinematic front-page.php lost the loop --hybrid depends on"

echo PASS
```

Three habits that make the difference:

- **Assert both directions.** A positive grep alone is satisfied by deleting the feature.
  Where a wrong old form existed, assert it is *gone* as well as that the right one is present
  — `tests/checks/wp-contributing.sh` requires the "Use when" clause *and* refuses the
  `trigger:` key it replaced:

  ```bash
  ! grep -Eq '^trigger:' "$s" || fail "$s still declares trigger:, a key Claude Code ignores"
  ```
- **Anchor on behavior, not headings.** A check that fails when someone renames a heading gets
  muted. Degrade to searching the whole file when the anchor is missing.
- **Prefer `grep -F`** for text containing backticks, `$`, brackets or `--flags`. A clever
  regex that matches by accident today stops matching after an innocent rewording.

When the thing you changed is a real script (`bin/*.sh`), test its behavior, not its source:
build a temp directory, put a fake binary earlier on `PATH`, run the script, assert what it
did. `tests/checks/tailwind-rebuild.sh` does this with a fake `npm` and fake watcher processes.

### Frontmatter is the loader's contract

| Layer | Required | Notes |
|---|---|---|
| `commands/<name>.md` | `description`, `allowed-tools`, `argument-hint` | Add `Agent` to `allowed-tools` only if it actually dispatches subagents |
| `agents/<name>.md` | `name`, `description`, `tools`, `model` | `tools` in the order `Read, Write, Edit, Grep, Glob, Bash`; `model` is a cost tier — `opus` for planning, `sonnet` for authoring and judgment, `haiku` for mechanical work (`tests/checks/model-routing.sh`) |
| `skills/<name>/SKILL.md` | `name`, `description`, `user-invocable: false` | The description says what the skill does and ends with "Use when …" — it is the only text Claude sees before deciding to load the skill. `trigger:` is not a frontmatter field and is ignored. Body under 500 lines; detail goes in `references/` (`tests/checks/skill-authoring.sh`) |

A layer file missing its frontmatter is **inert, not broken** — nothing errors, it simply
never loads. That is why `bin/doc-sync-check.sh` asserts it mechanically. Frontmatter that
does not parse as YAML is the same failure: an unquoted value containing `: ` (or ` #`, or
starting with a backtick) breaks the block, and Claude Code then loads the skill with **no
fields set** — no description, and `user-invocable` back at its default of true. Quote the
value; `tests/checks/frontmatter-yaml.sh` fails on it in every layer.

Read `references/skill-review.md` when creating, changing or reviewing a skill: it is the
checklist `/wp-contribute review` hands a fresh reviewer, and the part of the best practices
no grep can check.

### The traps that cost the most

- **Two i18n systems, never mixed.** `suffix` = one page with ACF fields duplicated as
  `hero_title_es`. `polylang` = one post per language joined by translation groups. Which one
  a project uses is a *recorded decision* — the `i18n strategy` line in the project's
  `.claude/CLAUDE.md` — not a guess. Absent line means `suffix`. The seam is a single file,
  `inc/i18n.php`; the Polylang variants live in `starter-theme/_i18n-variants/` and are kept
  **out** of the theme directories on purpose, because a starter carrying two definitions of
  the same function fatals the moment anything globs `inc/*.php`.
- **One deliberate crossover:** under Polylang, ACF **options-page** fields keep their
  `_<lang>` suffixes. Options are global, so Polylang's per-post model does not reach them.
- **Template routing changes which agent runs.** `tailwind` → `wp-tailwind` in author mode;
  `cinematic` → an entirely different shape (one continuous reel, `/wp-cinematic-scene` per
  scene, `/wp-section --hybrid` for trailing blocks) that the section walk must refuse to
  enter. Treat `basic` as an alias for `tailwind` rather than failing on it.
- **Starter-theme edits use the placeholder tokens** — `__starter__`, `__STARTER__`,
  `__STARTER_NAME__`, `__STARTER_DOMAIN__` — replaced by `/wp-init`. A real slug committed
  into the starter ships to every future project.
- **PHP 7.4 floor** in `skills/wp-polylang/scripts/*.php`: no `match`, no union types. Those
  run inside a real WordPress through `wp eval-file`, and `pll-lib.php` bridges `eval-file`'s
  local `$args` into `$GLOBALS` — the scripts do not work without that require.
- **Markup that lives in the database is invisible to every build step.** A CF7 form is a post
  row: Tailwind utility classes written into it compile only while some theme file happens to
  use the same ones. One hook class per element, declared in the theme CSS.

### Before you open a PR

1. Run every check and the doc-sync gate, from the repo root, exactly like this — a bare
   `for` loop's exit status is its *last* check's, so a red check earlier in the alphabet
   reads as green:

   ```bash
   fail=0; for f in tests/checks/*.sh; do bash "$f" || { echo "FAILED: $f"; fail=1; }; done
   [ "$fail" = 0 ] && bash bin/doc-sync-check.sh
   ```

   No `FAILED:` line, and doc-sync passes. Fix what fails and run it again until it does.
2. `bin/doc-sync-check.sh` is the half the greps cannot do — README tables,
   `docs/commands.md`, frontmatter and the version references all agree. It compares the
   CHANGELOG against `origin/main`; on a PR stacked on another branch pass
   `--changelog-base <parent-branch>`, because with no ref it can resolve it skips that rule.
3. **New behavior has a new check.** This is the one reviewers actually block on.
   A new or changed skill also passes `/wp-contribute review <name>` — the review no grep
   can do, against [references/skill-review.md](references/skill-review.md).
4. `CHANGELOG.md` gains an `[Unreleased]` entry. Say what changed and *why it was wrong
   before*; a release note that only names the feature is useless six months later.
5. Docs follow the change: a new command needs a row in the README table **and** an entry in
   `docs/commands.md`, and a step in `docs/workflows.md` if it belongs to a build path.
6. **Do not bump the version.** Four files state it and the maintainer moves all four at
   release time; a bump in a PR is a guaranteed conflict.

Commits follow [Conventional Commits](https://www.conventionalcommits.org/) — `feat:`, `fix:`,
`docs:`, `chore:`, `refactor:` — in English, and carry **no AI attribution**: no
`Co-Authored-By`, no "Generated with", nothing referencing the tool that helped write them.
Using AI to write a contribution is welcome; shipping its raw output is not.

### What gets a PR sent back

Missing check · docs out of sync (`bin/doc-sync-check.sh` fails) · a command reimplementing a
builder instead of dispatching it · a skill that acts · a version bump · hallucinated WP-CLI
flags or ACF methods · untested against a real WordPress site · one PR doing three things.

---

## Part 2 — Maintainers

### Merging, and the stacked-PR hazard

Small PRs, one concern each. When a PR is stacked on another (its base is the first PR's
branch, because it edits files that only exist there), **merge the base first** — but *how*
matters:

- **Merge commit** (the default; a rebase merge keeps the commits too) → the base's commits
  land on `main` unchanged, GitHub retargets the child to `main`, and its diff cleanly shrinks
  to just its own work.
- **Squash** → the base's commits are replaced by one new SHA. The child's branch still
  carries the originals, so it re-proposes the parent's entire diff. Recover with
  `git rebase --onto main <last-parent-sha> <child-branch>` and force-push with lease.

GitHub only auto-retargets a child when the base branch is **deleted**, so keep
**Settings → Automatically delete head branches** on. Without it, a squash-merged base sits
around and the child silently keeps the wrong base.

### Releases

`/wp-contribute release` is the procedure, step by step: run it rather than releasing by
hand. What it rests on is the part worth knowing first:

- **The version is stated in all four places at once** — `.claude-plugin/plugin.json`,
  `.claude-plugin/marketplace.json` (**twice**: `metadata.version` and the plugin entry), and
  the README badge. `bin/doc-sync-check.sh` fails if they disagree.
- **`## [Unreleased]` is never renamed.** The release heading is inserted below it and the
  entries move down. Every open PR edits under that heading, so a rename conflicts all of
  them on `CHANGELOG.md`.
- **`BACKLOG.md` is reconciled at every release**, and its `**Reconciled** on` line moves to
  the release date. `tests/checks/backlog-freshness.sh` fails as soon as the newest release
  heading is dated after that line — it became a check because the last hand reconciliation
  went stale in a single day.
- **The gates run again after the bump.** The version and backlog checks can only go red once
  the version files, `CHANGELOG.md` and `BACKLOG.md` have changed.
- **"Live" means three things**: the GitHub release is published, not a draft; the tag
  resolves on the remote; and the raw `.claude-plugin/*.json` on GitHub serve the new
  version, which is what a user's install resolves against. The GitHub release is the
  artifact, so verify all three by hand.
- `gh` runs as `env -u GH_TOKEN gh …`, so it authenticates with its own stored login rather
  than with whatever `GH_TOKEN` the shell happens to export.

Then delete the merged branch, and check no others accumulated: a branch whose PR is merged —
or closed but superseded — is safe to delete once `git diff main <branch>` shows nothing on it
that `main` lacks.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.