From 8ff19ea6a25fcc54d75992bb68795356649cff59 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 1 Oct 2026 20:13:23 +0530 Subject: [PATCH] feat!: import Plugin Elementary v2 Import the v2 starter plugin from features-plugin-skeleton as plugin-elementary --- .browserslistrc | 1 + .claude/skills/README.md | 21 + .claude/skills/init/SKILL.md | 118 + .claude/skills/scaffold/SKILL.md | 266 + .claude/skills/scaffold/evals/evals.json | 49 + .claude/skills/setup/SKILL.md | 388 + .claude/skills/setup/evals/evals.json | 49 + .editorconfig | 18 + .env.local.example | 28 + .github/CODEOWNERS | 1 - .github/ISSUE_TEMPLATE/bug.md | 24 + .github/ISSUE_TEMPLATE/bug.yml | 50 + .github/ISSUE_TEMPLATE/enhancement.yml | 25 + .github/ISSUE_TEMPLATE/epic.md | 24 + .github/ISSUE_TEMPLATE/feature.yml | 25 + .github/ISSUE_TEMPLATE/task.md | 24 + .github/PULL_REQUEST_TEMPLATE.md | 32 + .github/copilot-instructions.md | 27 + .github/dependabot.yml | 27 + .../primitives-php.instructions.md | 56 + .../instructions/structure.instructions.md | 21 + .github/prompts/init.prompt.md | 89 + .github/prompts/scaffold.prompt.md | 74 + .github/workflows/documentation.yml | 93 + .github/workflows/pr-title.yml | 28 - .github/workflows/test-measure.yml | 41 + .gitignore | 45 + .lintstagedrc.js | 14 + .npmrc | 13 + .nvmrc | 1 + .stylelintignore | 3 + .stylelintrc.json | 9 + .wp-env.json | 24 + AGENTS.md | 80 + CHANGELOG.md | 25 + CLAUDE.md | 21 + CONTRIBUTING.md | 51 + DEVELOPMENT.md | 329 + LICENSE.md | 354 + README.md | 98 +- babel.config.js | 25 + bin/block-scaffold/block.js | 332 + .../templates/dynamic-block/edit.js.mustache | 49 + .../dynamic-block/editor.scss.mustache | 9 + .../templates/dynamic-block/index.js.mustache | 40 + .../templates/dynamic-block/save.js.mustache | 3 + .../dynamic-block/style.scss.mustache | 12 + .../templates/meta-block/edit.js.mustache | 70 + .../templates/meta-block/index.js.mustache | 31 + .../templates/static-block/edit.js.mustache | 44 + .../static-block/editor.scss.mustache | 9 + .../templates/static-block/index.js.mustache | 40 + .../templates/static-block/save.js.mustache | 37 + .../static-block/style.scss.mustache | 12 + bin/dev-tools-e2e.sh | 309 + bin/features/dev-tools.js | 282 + bin/features/tailwind/postcss.config.js | 1 + bin/features/tailwind/tailwind.css | 3 + bin/init.js | 99 + bin/phpcbf.sh | 16 + bin/scaffold.config.js | 245 + bin/sync-ai.js | 23 + composer.json | 80 + composer.lock | 3952 + docs/blocks-and-assets.md | 88 + docs/dev-tools-demo.md | 206 + docs/features.md | 65 + docs/getting-started.md | 108 + docs/hmr.md | 189 + docs/index.md | 54 + docs/initialization.md | 126 + docs/internal/README.md | 26 + docs/internal/dev-tools-e2e.md | 112 + docs/internal/knowledge-graph.md | 63 + docs/internal/maintenance.md | 178 + docs/local-development.md | 156 + docs/scaffolding.md | 131 + docs/tailwind.md | 55 + elementary-plugin.php | 77 + eslint.config.mjs | 20 + graphify-out/GRAPH_REPORT.md | 1310 + graphify-out/graph.json | 104837 +++++++++++++++ inc/Autoloader.php | 64 + inc/Core/Assets.php | 264 + inc/Core/Components.php | 49 + inc/Core/Encryption.php | 42 + inc/Core/Logger.php | 32 + inc/Core/PluginSetup.php | 65 + inc/Core/Templates.php | 51 + inc/Helpers/Util.php | 129 + inc/Main.php | 84 + inc/Modules/Admin.php | 32 + inc/Modules/Admin/ExampleAdminPage.php | 78 + inc/Modules/Blocks.php | 32 + inc/Modules/Blocks/ExampleDynamicBlock.php | 51 + inc/Modules/CLI.php | 72 + inc/Modules/CLI/Healthcheck.php | 69 + inc/Modules/Cache.php | 33 + inc/Modules/Cache/ExampleCache.php | 84 + inc/Modules/Cron.php | 31 + inc/Modules/Cron/ExampleCronJob.php | 91 + inc/Modules/PostTypes.php | 33 + inc/Modules/PostTypes/ExamplePostType.php | 60 + inc/Modules/PostTypes/ExamplePostTypeTwo.php | 60 + inc/Modules/REST.php | 31 + inc/Modules/REST/ExampleRESTController.php | 84 + inc/Modules/Roles.php | 31 + inc/Modules/Roles/ExampleUserRole.php | 60 + inc/Modules/Settings.php | 31 + inc/Modules/Settings/ExampleSettingsPage.php | 191 + inc/Modules/Shortcodes.php | 31 + inc/Modules/Shortcodes/ExampleShortcode.php | 76 + inc/Modules/Taxonomies.php | 33 + inc/Modules/Taxonomies/ExampleTaxonomy.php | 54 + inc/Modules/Taxonomies/ExampleTaxonomyTwo.php | 47 + inc/Modules/Transients.php | 33 + inc/Modules/Transients/ExampleTransients.php | 77 + languages/elementary-plugin.pot | 217 + package-lock.json | 32215 +++++ package.json | 91 + phpcs.xml.dist | 134 + phpstan.neon.dist | 23 + phpunit.xml.dist | 27 + scripts/graphify/README.md | 86 + scripts/graphify/install.ps1 | 111 + scripts/graphify/install.sh | 135 + scripts/graphify/uninstall.sh | 47 + scripts/graphify/verify.sh | 66 + src/blocks/example-block-dynamic/block.json | 18 + src/blocks/example-block-dynamic/edit.js | 40 + src/blocks/example-block-dynamic/editor.scss | 9 + src/blocks/example-block-dynamic/index.js | 43 + src/blocks/example-block-dynamic/save.js | 3 + src/blocks/example-block-dynamic/style.scss | 12 + .../example-block-interactive/block.json | 20 + src/blocks/example-block-interactive/edit.js | 38 + .../example-block-interactive/editor.scss | 12 + src/blocks/example-block-interactive/index.js | 37 + .../example-block-interactive/render.php | 44 + .../example-block-interactive/style.scss | 12 + src/blocks/example-block-interactive/view.js | 21 + src/blocks/example-block/block.json | 18 + src/blocks/example-block/edit.js | 47 + src/blocks/example-block/editor.scss | 9 + src/blocks/example-block/index.js | 43 + src/blocks/example-block/save.js | 37 + src/blocks/example-block/style.scss | 12 + src/css/admin.scss | 3 + src/css/main.scss | 3 + src/js/admin.js | 6 + src/js/main.js | 6 + src/js/modules/module.js | 11 + templates/.gitkeep | 0 .../block-templates/example-block-dynamic.php | 16 + tests/bootstrap.php | 79 + tests/js/jest.config.js | 32 + tests/js/scaffold-config.test.js | 336 + tests/js/setup-globals.js | 2 + tests/php/AdminTest.php | 119 + tests/php/AssetsHmrTest.php | 129 + tests/php/BlocksTest.php | 109 + tests/php/CLITest.php | 96 + tests/php/CacheTest.php | 146 + tests/php/ComponentsTest.php | 68 + tests/php/CronTest.php | 136 + tests/php/EncryptionTest.php | 79 + tests/php/LoggerTest.php | 142 + tests/php/MainTest.php | 82 + tests/php/PostTypesTest.php | 140 + tests/php/RESTTest.php | 110 + tests/php/RolesTest.php | 153 + tests/php/SettingsTest.php | 138 + tests/php/ShortcodesTest.php | 122 + tests/php/TaxonomiesTest.php | 125 + tests/php/TemplatesTest.php | 72 + tests/php/TestCase.php | 21 + tests/php/TransientsTest.php | 138 + webpack.blocks.config.js | 88 + webpack.config.js | 282 + 179 files changed, 155061 insertions(+), 30 deletions(-) create mode 100644 .browserslistrc create mode 100644 .claude/skills/README.md create mode 100644 .claude/skills/init/SKILL.md create mode 100644 .claude/skills/scaffold/SKILL.md create mode 100644 .claude/skills/scaffold/evals/evals.json create mode 100644 .claude/skills/setup/SKILL.md create mode 100644 .claude/skills/setup/evals/evals.json create mode 100644 .editorconfig create mode 100644 .env.local.example delete mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/enhancement.yml create mode 100644 .github/ISSUE_TEMPLATE/epic.md create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/ISSUE_TEMPLATE/task.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/dependabot.yml create mode 100644 .github/instructions/primitives-php.instructions.md create mode 100644 .github/instructions/structure.instructions.md create mode 100644 .github/prompts/init.prompt.md create mode 100644 .github/prompts/scaffold.prompt.md create mode 100644 .github/workflows/documentation.yml delete mode 100644 .github/workflows/pr-title.yml create mode 100644 .github/workflows/test-measure.yml create mode 100644 .gitignore create mode 100644 .lintstagedrc.js create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .stylelintignore create mode 100644 .stylelintrc.json create mode 100644 .wp-env.json create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 DEVELOPMENT.md create mode 100644 LICENSE.md create mode 100644 babel.config.js create mode 100755 bin/block-scaffold/block.js create mode 100644 bin/block-scaffold/templates/dynamic-block/edit.js.mustache create mode 100644 bin/block-scaffold/templates/dynamic-block/editor.scss.mustache create mode 100644 bin/block-scaffold/templates/dynamic-block/index.js.mustache create mode 100644 bin/block-scaffold/templates/dynamic-block/save.js.mustache create mode 100644 bin/block-scaffold/templates/dynamic-block/style.scss.mustache create mode 100644 bin/block-scaffold/templates/meta-block/edit.js.mustache create mode 100644 bin/block-scaffold/templates/meta-block/index.js.mustache create mode 100644 bin/block-scaffold/templates/static-block/edit.js.mustache create mode 100644 bin/block-scaffold/templates/static-block/editor.scss.mustache create mode 100644 bin/block-scaffold/templates/static-block/index.js.mustache create mode 100644 bin/block-scaffold/templates/static-block/save.js.mustache create mode 100644 bin/block-scaffold/templates/static-block/style.scss.mustache create mode 100755 bin/dev-tools-e2e.sh create mode 100644 bin/features/dev-tools.js create mode 100644 bin/features/tailwind/postcss.config.js create mode 100644 bin/features/tailwind/tailwind.css create mode 100755 bin/init.js create mode 100755 bin/phpcbf.sh create mode 100644 bin/scaffold.config.js create mode 100755 bin/sync-ai.js create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 docs/blocks-and-assets.md create mode 100644 docs/dev-tools-demo.md create mode 100644 docs/features.md create mode 100644 docs/getting-started.md create mode 100644 docs/hmr.md create mode 100644 docs/index.md create mode 100644 docs/initialization.md create mode 100644 docs/internal/README.md create mode 100644 docs/internal/dev-tools-e2e.md create mode 100644 docs/internal/knowledge-graph.md create mode 100644 docs/internal/maintenance.md create mode 100644 docs/local-development.md create mode 100644 docs/scaffolding.md create mode 100644 docs/tailwind.md create mode 100644 elementary-plugin.php create mode 100644 eslint.config.mjs create mode 100644 graphify-out/GRAPH_REPORT.md create mode 100644 graphify-out/graph.json create mode 100644 inc/Autoloader.php create mode 100644 inc/Core/Assets.php create mode 100644 inc/Core/Components.php create mode 100644 inc/Core/Encryption.php create mode 100644 inc/Core/Logger.php create mode 100644 inc/Core/PluginSetup.php create mode 100644 inc/Core/Templates.php create mode 100644 inc/Helpers/Util.php create mode 100644 inc/Main.php create mode 100644 inc/Modules/Admin.php create mode 100644 inc/Modules/Admin/ExampleAdminPage.php create mode 100644 inc/Modules/Blocks.php create mode 100644 inc/Modules/Blocks/ExampleDynamicBlock.php create mode 100644 inc/Modules/CLI.php create mode 100644 inc/Modules/CLI/Healthcheck.php create mode 100644 inc/Modules/Cache.php create mode 100644 inc/Modules/Cache/ExampleCache.php create mode 100644 inc/Modules/Cron.php create mode 100644 inc/Modules/Cron/ExampleCronJob.php create mode 100644 inc/Modules/PostTypes.php create mode 100644 inc/Modules/PostTypes/ExamplePostType.php create mode 100644 inc/Modules/PostTypes/ExamplePostTypeTwo.php create mode 100644 inc/Modules/REST.php create mode 100644 inc/Modules/REST/ExampleRESTController.php create mode 100644 inc/Modules/Roles.php create mode 100644 inc/Modules/Roles/ExampleUserRole.php create mode 100644 inc/Modules/Settings.php create mode 100644 inc/Modules/Settings/ExampleSettingsPage.php create mode 100644 inc/Modules/Shortcodes.php create mode 100644 inc/Modules/Shortcodes/ExampleShortcode.php create mode 100644 inc/Modules/Taxonomies.php create mode 100644 inc/Modules/Taxonomies/ExampleTaxonomy.php create mode 100644 inc/Modules/Taxonomies/ExampleTaxonomyTwo.php create mode 100644 inc/Modules/Transients.php create mode 100644 inc/Modules/Transients/ExampleTransients.php create mode 100644 languages/elementary-plugin.pot create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 phpcs.xml.dist create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist create mode 100644 scripts/graphify/README.md create mode 100644 scripts/graphify/install.ps1 create mode 100755 scripts/graphify/install.sh create mode 100755 scripts/graphify/uninstall.sh create mode 100755 scripts/graphify/verify.sh create mode 100644 src/blocks/example-block-dynamic/block.json create mode 100644 src/blocks/example-block-dynamic/edit.js create mode 100644 src/blocks/example-block-dynamic/editor.scss create mode 100644 src/blocks/example-block-dynamic/index.js create mode 100644 src/blocks/example-block-dynamic/save.js create mode 100644 src/blocks/example-block-dynamic/style.scss create mode 100644 src/blocks/example-block-interactive/block.json create mode 100644 src/blocks/example-block-interactive/edit.js create mode 100644 src/blocks/example-block-interactive/editor.scss create mode 100644 src/blocks/example-block-interactive/index.js create mode 100644 src/blocks/example-block-interactive/render.php create mode 100644 src/blocks/example-block-interactive/style.scss create mode 100644 src/blocks/example-block-interactive/view.js create mode 100644 src/blocks/example-block/block.json create mode 100644 src/blocks/example-block/edit.js create mode 100644 src/blocks/example-block/editor.scss create mode 100644 src/blocks/example-block/index.js create mode 100644 src/blocks/example-block/save.js create mode 100644 src/blocks/example-block/style.scss create mode 100644 src/css/admin.scss create mode 100644 src/css/main.scss create mode 100644 src/js/admin.js create mode 100644 src/js/main.js create mode 100644 src/js/modules/module.js create mode 100644 templates/.gitkeep create mode 100644 templates/block-templates/example-block-dynamic.php create mode 100644 tests/bootstrap.php create mode 100644 tests/js/jest.config.js create mode 100644 tests/js/scaffold-config.test.js create mode 100644 tests/js/setup-globals.js create mode 100644 tests/php/AdminTest.php create mode 100644 tests/php/AssetsHmrTest.php create mode 100644 tests/php/BlocksTest.php create mode 100644 tests/php/CLITest.php create mode 100644 tests/php/CacheTest.php create mode 100644 tests/php/ComponentsTest.php create mode 100644 tests/php/CronTest.php create mode 100644 tests/php/EncryptionTest.php create mode 100644 tests/php/LoggerTest.php create mode 100644 tests/php/MainTest.php create mode 100644 tests/php/PostTypesTest.php create mode 100644 tests/php/RESTTest.php create mode 100644 tests/php/RolesTest.php create mode 100644 tests/php/SettingsTest.php create mode 100644 tests/php/ShortcodesTest.php create mode 100644 tests/php/TaxonomiesTest.php create mode 100644 tests/php/TemplatesTest.php create mode 100644 tests/php/TestCase.php create mode 100644 tests/php/TransientsTest.php create mode 100644 webpack.blocks.config.js create mode 100644 webpack.config.js diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..0152f61 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +extends @wordpress/browserslist-config diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 0000000..959bdbc --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,21 @@ +# AI skills + +Skills for AI assistants (Claude Code, Cursor, and any tool that reads the Claude Code skill convention) that drive this plugin's setup and the `@rtcamp/wp-tooling` scaffold engine. Each skill is a directory with a `SKILL.md` (frontmatter `name:` + `description:`, portable Markdown body). + +| Skill | Invoke | What it does | +|---|---|---| +| [`init/`](init/SKILL.md) | `/init` | Turn a fresh Plugin Elementary clone into a named project and manage it afterwards: rename the starter tokens, keep/remove the example sets, toggle optional features (Tailwind, HMR, DevTools). Drives `npm run init`. | +| [`scaffold/`](scaffold/SKILL.md) | `/scaffold` | Add one feature (CPT, taxonomy, block, REST controller, shortcode, cron, CLI, settings page, user role, service, CI workflow) via `npx wp-tooling add`, TDD-first. | +| [`setup/`](setup/SKILL.md) | `/setup` | Generic tooling bootstrap for plugins and themes, shipped with `@rtcamp/wp-tooling`. Not a personalization path for this plugin; use `/init`. | + +`init` is specific to Plugin Elementary. `scaffold` tracks `@rtcamp/wp-tooling` and introspects the project, so it stays correct as the layout evolves. `setup` has no Copilot counterpart. + +Developer-facing descriptions of both routes are in [docs/initialization.md](../../docs/initialization.md) and [docs/scaffolding.md](../../docs/scaffolding.md). + +**Copilot parity:** `init` and `scaffold` also exist for GitHub Copilot as prompt files in [`.github/prompts/`](../../.github/prompts/) (`/init`, `/scaffold`), kept consistent with these skills. Shared conventions and the knowledge-graph (graphify) policy live in [`AGENTS.md`](../../AGENTS.md). + +## Safety + +These skills are opinionated about safety and never: run a package manager or `npm run build` without consent; read, log, or transmit secret values; apply cross-file wiring without showing the diff and getting consent; or commit, push, or open PRs without approval. `init` additionally never runs a destructive setup without confirming the resolved values against a clean working tree. + +Consented exceptions to the package-manager rule: `npm run init` (the project's own setup script), and the `npm install`/`composer install` `init` runs at the start of setup. After any code change, the skills refresh the LOCAL graph with `graphify update .`; they never commit it and never run the cross-repo `/graphify . --update` or `merge-graphs` (see [AGENTS.md](../../AGENTS.md)). diff --git a/.claude/skills/init/SKILL.md b/.claude/skills/init/SKILL.md new file mode 100644 index 0000000..cfb5d02 --- /dev/null +++ b/.claude/skills/init/SKILL.md @@ -0,0 +1,118 @@ +--- +name: init +description: Set up a fresh clone of Plugin Elementary as a named project, or manage its identity and capabilities later. Installs dependencies (`npm install`, `composer install`), then drives `npm run init`. Always confirms before destructive or install steps; expects a clean working tree. +--- + +# init + +Two jobs: **bootstrap + setup** a fresh clone into a real plugin, or **manage** an already-set-up project. Always interactive: gather inputs first, confirm the resolved plan, then act. This skill is a thin front end for `npm run init` - it does not reimplement what the engine already asks for. + +## Use for +- First run: install dependencies, rename starter tokens, pick capabilities to keep. +- First run WITH a feature brief: after rename + capabilities, implement the described features by chaining to the **scaffold** skill (TDD), replacing the matching `Example*` placeholders (step 5). +- Later: rename / re-prefix, toggle features (Tailwind, HMR, DevTools). + +## Do not use for +- Adding a feature class → the **scaffold** skill (`npx wp-tooling add`). +- Bug fixes, refactors, hand-written features. +- Re-running init to change capabilities after first setup. Removal is one-shot (markers are consumed); a second run leaves dangling `Main::CLASSES` refs. Set the set ONCE; change it later via scaffold. + +## Be interactive (required) +Ask the developer for every input the chosen path needs, in ONE batched message, before acting. Never assume a project name. Show derived values back in that same exchange. Wait for explicit consent before any install, file rewrite, or destructive step. If an answer is missing, ask; do not guess. + +## Plan and announce (required) - developer experience first +1. **State the plan up front** in one short message: the mode (setup / manage) and the ordered steps for THIS request - detect mode -> install deps -> ask + run init -> (setup with a brief) hand each feature to the scaffold skill. +2. **Maintain a live TODO list** (`TodoWrite` in Claude Code): one entry per planned step, exactly one `in_progress` at a time. +3. **Announce each step with a short title** and report its outcome in one line. Never go silent during a long step. + +## Steps + +### 1. Detect mode +```bash +test -f .wp-scaffold.json && echo manage || echo setup +``` +No `.wp-scaffold.json` → **setup**. Present → **manage** (read it for current identity; `npm run init -- --list` shows feature status). + +### 2. Install dependencies +With consent, run: +```bash +npm install +composer install +``` +Skip either that's already installed and current. If consent is declined, surface both as developer actions and stop - init cannot run without them. + +Then verify a clean working tree (`git status`) - init rewrites files irreversibly, and a clean tree is the only undo. If dirty, ask to commit/stash. + +### 3. Ask, confirm, run - one step +`npm run init` is a single command; don't turn gathering its inputs into a multi-round-trip wizard. + +**Setup:** in ONE message ask for project name (required, e.g. `Acme Blog` → namespace `rtCamp\Plugin\Acme_Blog`, package `rtcamp/acme-blog`, text domain, prefixes, main file - show these back), version (default `1.0.0`), which capability sets to remove, and which features to enable (defaults: keep all sets, `hmr` on, `tailwind` and `dev-tools` off - see Capability model). The engine derives the tokens itself; do not read the engine source (`identity.js` etc.) - the mapping above is the contract, and the graph answers any deeper question (see the graphify policy in `AGENTS.md`). + +**Manage:** ask which of identity / features to change. It's a single flag on an existing project - no wizard needed. + +Get explicit consent on the resolved values (init is destructive), then run: +```bash +# Setup: +npm run init -- --name="Acme Blog" --version=1.0.0 --yes \ + --remove-examples=cron,rest --features=hmr,tailwind,dev-tools +# Manage: +npm run init -- --list +npm run init -- --enable=dev-tools --yes +npm run init -- --features=hmr --yes # exact enabled set (empty = none) +``` +- `--keep-examples` keeps all; `--remove-examples` (no value) removes all; `--remove-examples=a,b` removes listed keys. +- `--features=a,b` sets the exact enabled set; `--enable`/`--disable` are deltas. +- `--yes` requires `--name`. For the guided wizard, run bare `npm run init` (space toggles, enter confirms) - the wizard already asks everything above, so prefer it over hand-building flags when the developer has no strong opinion on the exact command. +- `npm run init` also runs `npm run sync-ai`; that is expected. + +**Setup WITH a feature brief - clear the briefed examples in THIS step (not a separate round-trip):** keep the capabilities the brief needs (do NOT list them in `--remove-examples`); remove every other capability. Then, batched with the init run, strip the kept-briefed capabilities of their demo code so scaffold can fill them: for each, delete the `Example*` class file(s) under `inc/Modules//`, delete `tests/php/Test.php`, and set that module's `get_classes()` to `return [];` (drop the example `use` imports). The module stays wired in `Main::CLASSES` - only its example contents go, leaving an empty, already-wired module ready for scaffold. Example: brief = Testimonial CPT + Industry taxonomy + draft REST -> keep `post-types`/`taxonomies`/`rest`, `--remove-examples=blocks,shortcodes,cli,cron,settings,roles,cache,transients,admin`, then empty the PostTypes/Taxonomies/REST modules. + +### 4. After init +- Tailwind enabled → developer runs `npm install` (it added `src/css/tailwind.css`, `postcss.config.js`, and pinned `@rtcamp/tailwind-config`). +- DevTools enabled → it's still a private package (`rtcamp/wp-devtools`, VCS-sourced): developer runs `composer update rtcamp/wp-devtools`, which may need GitHub auth, then `npm run dev:connect`. +- `composer dump-autoload` (engine runs it when `composer.json` is present). +- Trust the engine's own output to verify (it reports the removed sets, drops their `Main::CLASSES` lines and tests, and regenerates the autoloader). To confirm a symbol or reference, run a single `graphify query`/`affected` against the graph - never grep `Main.php`/`inc/` to check removal. +- If you hand-edited PHP and are NOT handing off to scaffold (e.g. a manage-mode dangling `Main::CLASSES` cleanup), run `composer lint` on the change and fix it. When a brief follows (step 5), leave the lint/PHPStan/test gates to the scaffold skill - init does not run them. +- **Refresh the LOCAL graph** with `graphify update .` (plugin slice, tree-sitter, no API, seconds). **Never commit or push `graphify-out/graph.json`** - the committed file is a maintainer-maintained cross-repo baseline (rebuilt and pushed when this or a shared repo changes); your local refresh stays local. Do NOT run the cross-repo `/graphify . --update` subagent or `merge-graphs` (expensive; clobbers the baseline). If graphify is not installed, say so in one line; do not block. +- Report: new identity, capabilities removed/kept, features toggled, outstanding developer actions. + +### 5. Implement the brief by handing off to scaffold (setup only, when features were described) +By this point step 3 has already emptied the briefed capabilities (their `Example*` classes/tests are deleted and the modules are empty but still wired in `Main::CLASSES`). init's remaining job is purely to **hand each described feature to the scaffold skill** - it does NOT build the class, write or run tests, set up the test env (`npm run wp-env start`), or run the lint/PHPStan gates. The **scaffold skill takes over** and owns all of that. + +For each described feature, **invoke the scaffold skill**, passing the brief (e.g. a `testimonial` CPT; an `industry` taxonomy attached to `testimonial`; a REST controller that creates testimonials in `draft`). The scaffold skill owns conventions, the `wp-tooling add` call, wiring into the already-present module, the TDD loop, test execution, and the gates - report its result; do not duplicate that work here. + +**Pass your findings to scaffold (speeds it up).** init has already resolved the project identity while renaming, so hand those values to the scaffold skill in the same message instead of making it re-derive them. State explicitly: the resolved **PHP namespace root** (e.g. `rtCamp\Plugin\Acme_Testimonials`), **base_path** (`inc`), **tests_namespace** (e.g. `rtCamp\Plugin\Acme_Testimonials\Tests`), **tests_path** (`tests/php`), **text_domain** and **slug** (e.g. `acme-testimonials`), the **plugin display name**, and **which capabilities step 3 kept-and-emptied** (their `inc/Modules//` folders already exist and are wired in `Main::CLASSES`, so scaffold wires into them rather than creating a module). With these provided, the scaffold skill skips its own `composer.json` / plugin-header / `Main.php` discovery reads. It still reads source files when it needs exact code (the graph and a handoff are for orientation, not a substitute for reading). + +For a kept capability the brief did NOT mention, leave its `Example*` but tell the developer it is demo-only, not for deployment, and offer to scaffold a real class or drop the capability. + +End state: init has renamed, trimmed capabilities, and emptied the briefed examples (step 3); the scaffold skill has implemented the real features. + +## Capability model +ONE "Select the capabilities to include" prompt. Each is keep-or-remove; removing deletes it entirely (concrete classes, `inc/Modules/.php`, its `Main::CLASSES` line, coupled Core regions, feature deps). + +| Category | Keys | +|---|---| +| Content | `post-types`, `taxonomies` | +| Editor & Front-end | `blocks`, `shortcodes`, `tailwind` (feature) | +| APIs & Automation | `rest`, `cli`, `cron` | +| Admin | `settings`, `admin`, `roles` | +| Utilities | `cache`, `transients` | +| Developer Tooling | `test-measure` (CI workflow), `hmr` (feature), `dev-tools` (feature) | + +Sets are kept by default; pass keys to `--remove-examples` to drop. Features: `hmr` on, `tailwind` and `dev-tools` off; toggle via `--features`/`--enable`/`--disable`. `dev-tools` (`rtcamp/wp-devtools`, runtime telemetry over MCP - Query Monitor + the MCP Adapter) is the one feature still sourced from a private VCS repo; enabling it needs GitHub auth on `composer update` (see step 4). Everything else here installs from public registries. + +### Changing capabilities after setup +Add later → scaffold skill / `npx wp-tooling add /` (writes the class, wires the module). Remove later → delete its `inc/Modules//` classes + `Main::CLASSES` line by hand. Toggle a feature any time via `npm run init -- --enable=`/`--disable=`. Do not re-run init to change the capability set. + +## Hard rules +- **BASE (see AGENTS.md guardrails):** never run history/remote `git`/`gh` (commit, push, `branch -D`, `reset --hard`, PR, issue comment, `gh secret set`) - print them as developer actions. Never do a destructive operation outside this plugin directory. +- Never commit or push `graphify-out/graph.json`; local graph refreshes stay local. +- Package managers only with consent: `npm install`, `composer install` (step 2), and `npm run init`. Outside those, surface install commands as developer actions. +- Never run a destructive init without confirming resolved values, on a clean tree. +- Never commit, push, open PRs, or edit `.wp-scaffold.json` by hand. +- Never invent flags. Supported: `--name`, `--version`, `--yes`, `--keep-examples`, `--remove-examples[=...]`, `--features`, `--enable`, `--disable`, `--reinit`, `--list`, `--clean`, `--help`. Run `npm run init -- --help` if unsure. + +## Reference +- Engine: `@rtcamp/wp-tooling/init` (via `bin/init.js`). Capability map: `bin/scaffold.config.js`. DevTools feature: `bin/features/dev-tools.js`. +- Conventions renamed into: `AGENTS.md`, `.github/instructions/structure.instructions.md`. +- Graphify policy: `AGENTS.md`. diff --git a/.claude/skills/scaffold/SKILL.md b/.claude/skills/scaffold/SKILL.md new file mode 100644 index 0000000..db0463a --- /dev/null +++ b/.claude/skills/scaffold/SKILL.md @@ -0,0 +1,266 @@ +--- +name: scaffold +description: Add a scaffold (PHP class, block, CI workflow) to the current project using @rtcamp/wp-tooling. TDD-first - derive test cases from the developer brief, scaffold via the engine, write tests, then implement to green under a red-green-refactor loop. Never runs package-manager commands or writes secret values; surfaces them as developer actions. +--- + +# scaffold + +Drive `@rtcamp/wp-tooling`. Map the developer's request to a scaffold, derive test cases first, invoke the engine, expand tests, implement to green, report. + +This skill is the canonical wp-tooling scaffold skill, tailored to this plugin's structure (`inc/Modules/`, namespace `\Modules\`, tests in `tests/php/`). + +## Use for + +CPT, taxonomy, REST controller, dynamic block, shortcode, cron, CLI command, admin/settings page, user role, plain Registrable service, framework module, CI/CD workflow. + +## Do not use for + +Refactors, bug fixes, hand-written features, anything no scaffold covers. + +## Workflow + +### 0. Plan and announce - before any other work + +Before discovery, scaffolding, or coding, write a TODO list covering every step you intend to run for this task and show it to the developer. Use the host's task-tracking surface (`TodoWrite` in Claude Code; the host equivalent elsewhere). + +Minimum entries: + +1. Introspect project (§2). +2. Derive test-case checklist (§4) and confirm with developer. +3. Scaffold call(s) - one entry per kind for multi-kind features. +4. Apply wiring (§6a) - one entry per `ai.wiring` snippet, with consent. +5. Expand tests, run red (§7 A-B). +6. Implement to green (§7 C-E), one test at a time. +7. Refactor + final gates (§7 F-G). +8. Report (§9). + +Update the list in real time. Mark each entry `in_progress` before starting it and `completed` the moment it is done. Exactly one entry `in_progress` at any time. Add new entries when the work reveals them (e.g. a missing kind-module, a stuck-loop escalation). + +This makes progress legible to the developer and gives them a stable surface to interrupt or re-prioritise. + +### 1. Discover + +```bash +npx wp-tooling list --json +``` + +Result: `{ scaffolds: [{ id, slug, category, kind, origin, counts }, ...] }`. Pick one `category/slug`. If ambiguous, ask the developer with candidates. Never guess. Entries with `origin: "remote"` live in another repo: their manifest is fetched on the first `add`, so `counts` is `null` here and the kind is always `template`. That first `add` does network I/O and can fail with `EFETCHFAIL` (see Engine errors) - treat a remote scaffold exactly like a local one once it resolves; the only difference is the fetch. + +### 2. Introspect once per session (cache result) + +**Reuse `/init`'s findings if it handed off to you in this session.** When `/init` chained here, you already established the resolved identity and conventions while running init - root namespace, base path (`inc/`), tests namespace + path (`\Tests` -> `tests/php/`), text domain, and constant prefix. They are facts you set, not guesses: reuse them and SKIP the `composer.json` / plugin-header / `Main.php` reads below that only recover them. You still read a sample implementation for the registration pattern (next paragraph). Because init deleted the briefed `Example*`, sample the pattern from the framework abstract for the kind (`vendor/rtcamp/wp-primitives/inc/Contracts/Abstracts/Abstract.php`) or any remaining example; do not guess. + +**The graph saves tokens for orientation only; read the file for any data you copy.** This repo commits a queryable graph (`graphify-out/graph.json`). Use it to LOCATE things cheaply so you open fewer files: which classes implement a kind, what references a symbol, the path between two classes - `graphify query "..."`, `graphify explain ""`, `graphify affected ""`, `graphify path "A" "B"`. The graph is structural (not the full source), so it is NOT a source of truth for the patterns you act on. For registration shape, exact namespace, how a module's `get_classes()` lists classes, and wiring location, **read the actual file** - 100% accuracy beats a few saved tokens. (If you or init changed code this session, refresh the local slice first with `graphify update .`, seconds, **never push `graphify-out/graph.json`** - AGENTS.md graphify policy.) + +Read, in order (use the graph to find them fast; read the files for their contents): + +- `composer.json` -> `autoload.psr-4` (root namespace + base path; here `\` -> `inc/`, e.g. `rtCamp\Plugin\RT_Testimonial\` -> `inc/`). `autoload-dev.psr-4` for the tests namespace (`\Tests\` -> `tests/php/`). +- `package.json` -> scripts. For block scaffolds, parse `build:blocks` for `--output-path=` and pass as `--build_dir`. Default `assets/build/blocks`. +- Main plugin file -> bootstrap class (`Main`) + `Main::CLASSES`. +- 2-3 existing implementations of the same kind under `inc/Modules//` (e.g. `Example*`): registration pattern, class-name suffix, sub-namespace, and how the module's `get_classes()` lists them. **Read these files** - they are the source of truth for the patterns you copy; use `graphify` only to find them. +- Block scaffolds: sample one `src/blocks/*/block.json` for vendor prefix and source dir. +- CI scaffolds: sample one `.github/workflows/*.yml` for filename and trigger style. + +Anchors (`// scaffold::classes`) are hints, not ground truth. Sampled patterns win. + +Confirm findings with the developer in one short message. Proceed on confirmation. + +### 3. Canonical layout + +Files group by **kind**, never by feature. `` = this plugin's `composer.json` `autoload.psr-4` root (e.g. `rtCamp\Plugin\RT_Testimonial` -> `inc/`); tests autoload via `\Tests` -> `tests/php/`. **The engine's own defaults target a different layout (`includes/...`, `Inc\...`, `tests/...`), so you MUST pass this plugin's conventions on every `add` (§5).** + +| Scaffold | Source dir | Source ns | Test dir | Test ns | Module file (wire here) | +|---|---|---|---|---|---| +| `wp/cpt` | `inc/Modules/PostTypes/` | `\Modules\PostTypes` | `tests/php/` | `\Tests` | `inc/Modules/PostTypes.php` | +| `wp/taxonomy` | `inc/Modules/Taxonomies/` | `\Modules\Taxonomies` | `tests/php/` | `\Tests` | `inc/Modules/Taxonomies.php` | +| `wp/block-dynamic` | `inc/Modules/Blocks/` + `src/blocks//` + `assets/build/blocks//` | `\Modules\Blocks` | `tests/php/` | `\Tests` | `inc/Modules/Blocks.php` | +| `wp/rest` | `inc/Modules/REST/` | `\Modules\REST` | `tests/php/` | `\Tests` | `inc/Modules/REST.php` | +| `wp/shortcode` | `inc/Modules/Shortcodes/` | `\Modules\Shortcodes` | `tests/php/` | `\Tests` | `inc/Modules/Shortcodes.php` | +| `wp/admin-page` | `inc/Modules/Admin/` | `\Modules\Admin` | `tests/php/` | `\Tests` | `inc/Modules/Admin.php` | +| `wp/settings-page` | `inc/Modules/Settings/` | `\Modules\Settings` | `tests/php/` | `\Tests` | `inc/Modules/Settings.php` | +| `wp/user-role` | `inc/Modules/Roles/` | `\Modules\Roles` | `tests/php/` | `\Tests` | `inc/Modules/Roles.php` | +| `wp/cli` | `inc/Modules/CLI/` | `\Modules\CLI` | `tests/php/` | `\Tests` | `inc/Modules/CLI.php` | +| `wp/cron` | `inc/Modules/Cron/` | `\Modules\Cron` | `tests/php/` | `\Tests` | `inc/Modules/Cron.php` | +| `wp/registrable` | `inc/Modules//` | `\Modules\` | `tests/php/` | `\Tests` | `inc/Modules/.php` | + +Match the existing directory case exactly (`REST`, `CLI`, not `Rest`/`Cli`). + +**Modules host one kind each. No `Modules//...`.** A multi-kind feature (e.g. Testimonials = CPT + taxonomy + REST) spans the per-kind directories and wires into each kind's module. + +If the project already has a per-feature module folder, flag as anti-pattern. Offer migration before adding new artifacts. Do not scaffold into it. + +### 4. Derive test cases from the developer brief - BEFORE any scaffold call + +Write a test-case checklist covering: + +- **Happy path** - central behaviour the developer stated. +- **Edge cases** - empty/missing/boundary inputs, large input. +- **Error paths** - invalid input, missing auth, wrong capability. +- **Integration** - kind-specific: + - `wp/cpt`: `post_type_exists()`, supports, REST exposure, attached taxonomies. + - `wp/taxonomy`: `taxonomy_exists()`, attached object types, term assignment. + - `wp/rest`: route appears in `rest_get_server()->get_routes()`, permission check, request/response schema, dedupe behaviour. + - `wp/block-dynamic`: block name, `register_hooks` action, `render()` markup with `WP_Query` fixture, empty state, count cap, attribute filters. + - `wp/cron`: `wp_next_scheduled()`, callback fires, unschedule works. + - `wp/cli`: `WP_CLI::add_command` registered, `__invoke` behaviour, dry-run flag. + +Show the checklist to the developer. Ask: confirm, add, remove? Resolve before scaffolding. This is the cheapest place to catch a misread requirement. + +### 5. Apply conventions, invoke the engine + +Always invoke the engine for any kind it covers. Hand-writing is not a substitute. + +**Always pass this plugin's conventions (§3)** - the engine defaults target a different layout. With `` = the autoload root and `` = the module dir: + +```bash +npx wp-tooling add wp/ --non-interactive --json \ + --namespace='\Modules\' --base_path=inc/Modules/ \ + --tests_namespace='\Tests' --tests_path=tests/php --text_domain= \ + --slug= --class= --= ... +``` + +Per-scaffold inputs are not listed by `--help`; run once with `--dry-run` to discover required inputs (a CPT needs `slug`/`singular`/`plural`; a taxonomy also needs `--object_type=` to attach it to a CPT; a REST controller needs `--name` + `--rest_namespace`). Dry-run preview: append `--dry-run`. Never use the interactive wizard. + +**Multi-kind feature run order:** + +1. `wp/module` for any missing kind-module (e.g. `--name=PostTypes --kind=cpt`). Most kept capabilities already ship a module (`inc/Modules/.php`). +2. Artifacts in dependency order: CPT before any taxonomy attaching to it; CPT before any block/REST controller querying it. +3. Re-read `ai.wiring` after each call. + +Result shape: `{ scaffold, engine, developer, ai, warnings }`. + +### 6. Process the result + +| Block | Action | +|---|---| +| `engine.wrote` / `engine.skipped` | Already on disk. Report. | +| `developer.install.composer` / `developer.install.npm` | Print as copy-paste command. **Never run `composer require` / `npm install`.** Notes: `npm install --install-links` is only needed for a local `file:` override; the framework (`rtcamp/wp-primitives`) already ships, so ignore a `composer require rtcamp/wp-primitives` suggestion. | +| `developer.secrets` | Print as `gh secret set` checklist. **Never read/write/log/transmit values.** | +| `ai.wiring` | Adaptive wiring with consent (see 6a). | +| `ai.tests` | Mandatory expansion under TDD loop (see 7). | +| `warnings` | Print to developer. | + +#### 6a. Adaptive wiring + +For each `{ targetFile, anchor, snippet, description }`: + +0. **Target file** - the engine miscomputes `targetFile` for this layout (it emits `inc/Modules/Modules/.php`, doubled). The real module file is `inc/Modules/.php` (§3). +1. **Snippet** - use canonical if it matches the sampled project pattern; else translate using the sampled shape with the new class substituted. Show both. If patterns conflict or no samples exist, ask. +2. **Location** - this plugin's modules carry no `// scaffold::classes` anchor, so insert the `::class` after the last existing entry in the module's `get_classes()` array (and add the matching `use` import). Anchor present -> after it; else after the last `::class` in `get_classes()`; else skip and print as a manual instruction. +3. **Consent** - show targetFile + line range + description + rendered snippet. Ask `[apply / different location / edit snippet / skip]`. Never apply without consent. +4. **Idempotent** - search first, do not re-insert. + +### 7. TDD loop (mandatory for every scaffolded artifact) + +Tests come before implementation. No exceptions. If the developer says "skip tests", explain the policy and decline. + +For block scaffolds, surface a developer action before testing: "run `npm run build` so the editor can read the compiled block." Do not run it yourself. + +| Step | Action | +|---|---| +| A | Expand the engine's stub into the full suite from §4's checklist. Strip every `markTestIncomplete`. | +| B | Run: `npm run test:php` (PHP, under wp-env) / `npm run test:js` (JS). Expect red. PHP tests need the WP test env: if the runner cannot connect, surface `npm run wp-env start` as a developer action and retry. | +| C | Implement just enough production code to flip **one** failing test green. | +| D | Re-run. Confirm that one test passes. | +| E | Loop B-D one test at a time. | +| F | Once green, refactor; re-run. | +| G | Final gates - all must pass: full PHPUnit suite (`npm run test:php`), full Jest suite if JS touched (`npm run test:js`), `composer format` (phpcbf), `composer lint` (phpcs), `composer phpstan` (PHPStan level 5), `npm run lint:js`. Never silence a real phpcs/phpstan finding with a blanket ignore; fix it, or escalate (§8). | + +Frameworks per kind: + +| Kind | Framework | +|---|---| +| `wp/cpt`, `wp/taxonomy`, `wp/cron`, `wp/cli`, `wp/rest`, `wp/shortcode`, `wp/admin-page`, `wp/settings-page`, `wp/user-role`, `wp/registrable` | PHPUnit | +| `wp/block-dynamic` | Jest (edit.js) + PHPUnit (render method) | +| `block/interactive` | Jest + Playwright | +| `ci/*` | actionlint + yaml-parse | + +### 8. Escalate when stuck - do not guess + +Stop and report findings to the developer when any of the following holds. Wait for response before continuing. + +- 3 consecutive iterations of step C-D on the same test without progress. +- A test result contradicts your model of the code (likely hallucination - re-read the file on disk before guessing again). +- Sampled project patterns conflict and you cannot resolve which to follow. +- A `ai.wiring` snippet's anchor and project pattern both differ from canonical. +- Developer requirements remain ambiguous after one round of clarification. + +Escalation report format: **what you tried, what you observed, what's blocking, 1-3 specific resolution options.** Do not keep iterating in silence. + +### 9. Refresh the graph, then report + +Once green, refresh the LOCAL graph so it reflects the new artifacts (and any later query stays accurate): `graphify update .` (tree-sitter, seconds). **Never commit or push `graphify-out/graph.json`** - it is a maintainer-maintained cross-repo baseline; your refresh stays local (AGENTS.md graphify policy). Do not run the cross-repo `/graphify . --update` subagent or `merge-graphs`. + +Then report: + +- Files written, grouped by top-level directory. +- Wiring applied: file, line, pattern used. +- Tests authored and pass count per file. +- Lint + PHPStan result. +- Outstanding developer actions: composer / npm installs (`--install-links` only for a local `file:` override), `npm run build` (blocks), secrets to set, branch-protection note (CI). + +## Environment + engine quirks (this plugin) + +**Test env (`wp-env`):** +- Always use `npx wp-env` (or `node_modules/.bin/wp-env`), never bare `wp-env`. +- If `wp-env start` reports a port already allocated, start on free alternates: `WP_ENV_PORT=8890 WP_ENV_TESTS_PORT=8891 npm run wp-env start` (find a free pair with `lsof -nP -iTCP: -sTCP:LISTEN`). +- `wp-env start` can flake on a transient image pull (TLS timeout); one retry is allowed, and exit 0 does not mean "up" - confirm `WordPress test site started` appears in the output. +- `pretest:php` skips `composer install` when `vendor/` is already mounted; if `npm run test:php` still fails on it, run PHPUnit directly: `npx wp-env run tests-cli --env-cwd=wp-content/plugins/$(basename "$PWD") -- vendor/bin/phpunit -c phpunit.xml.dist`. + +**Generated-code quirks (write the code right up front; these survive `composer format`):** +- **Fully-qualify WP global classes** (`\WP_Error`, `\WP_REST_Request`, `\WP_REST_Response`) everywhere they appear - in code AND docblocks - with NO `use` statement for them. Reason: `composer format` force-qualifies `WP_Error` (Slevomat `FullyQualifiedExceptions` treats `*Error` as an exception) and then strips the now-unused imports, including docblock-only ones like `WP_REST_Request` (`UnusedUses` runs without `searchAnnotations`); PHPStan (scanning `inc/`) then reports `class.notFound` / `return.type` for `\WP_Error`. Writing them fully-qualified avoids the format -> phpstan round-trip. +- **`wp/rest` overridden controller methods must match WP core's untyped signatures.** The template emits typed overrides like `get_items_permissions_check( WP_REST_Request $request ): bool`; adding a parameter type to a `WP_REST_Controller` override is a fatal LSP/contravariance violation that crashes `wp-env start` during provisioning. Rewrite every overridden method (`get_item`/`create_item`/`update_item`/`delete_item` + their `*_permissions_check`) with **untyped** params/return and express the types in PHPDoc only. +- **`wp/rest` route is double-versioned.** The generated `$namespace` already includes the version (e.g. `testimonials/v1`) yet `register_routes()` appends `/v{version}` again -> `v1/v1`. Pick one source: set `$namespace` to the base only and keep `/v{version}`, or register with `$this->namespace` directly. Fix the generated test's asserted route too. +- The engine emits compact `declare(strict_types=1);` and test files with no `@package` tag or per-method doc comments. Run `composer format` (phpcbf) for the spacing, then hand-add the `@package` file tag + a one-line doc comment on each `test_*` method (PHPCS requires them; phpcbf does not add them). +- **File-header order:** the file docblock must immediately follow ` `TestimonialFeedController`); `--slug` and `--text_domain` are not `wp/rest` inputs (warned + ignored). The generated controller is GET-only - replace it with the create/POST flow the brief needs. +- **REST tests:** the generated test calls `register_routes()` directly, which trips WP's `_doing_it_wrong` under `WP_UnitTestCase`. Register via the action instead: in `set_up()`, `global $wp_rest_server; $wp_rest_server = new WP_REST_Server(); do_action( 'rest_api_init' );`, and reset it to `null` in `tear_down()`. + +**Judge your work scoped, not repo-wide.** Run the gates against the files you authored (`vendor/bin/phpcs `, `vendor/bin/phpstan analyse `) and scope `composer format` to them; a repo-wide non-zero exit is not necessarily your code. Plugin Elementary ships gate-clean, so any finding outside your files is a regression to report, not ambient noise. + +## Hard rules - never violate + +- **BASE (see AGENTS.md guardrails):** never run history/remote `git`/`gh` (commit, push, `branch -D`, `reset --hard`, PR, issue comment, `gh secret set`, `gh repo edit`); print them as developer actions. `git clone`/`checkout` are fine. Never do a destructive operation outside this plugin directory. Never commit or push `graphify-out/graph.json`. +- Never write production code before its test exists on disk. +- Never hand-write an artifact the engine can scaffold. +- Never group multiple kinds under a per-feature folder (`Modules//...`). +- Never declare an artifact done without its test file passing. +- Never declare PHP done without the §7 G gates (`composer format` -> `composer lint` -> `composer phpstan`) clean on the generated code. +- Never lower assertion strength to make a test pass (`assertTrue(true)`, widened types). +- Never delete or skip a test the developer would expect to pass. +- Never leave `markTestIncomplete`, `markTestSkipped`, or `@todo` in committed state. +- Never run `composer require`, `npm install`, `npm run build`, or any write-side CLI without explicit consent. +- Never read, write, log, or transmit secret values. +- Never edit branch protection, repo settings, webhooks, or any GitHub admin surface. +- Never apply wiring without showing the diff and getting consent. +- Never invent a third registration pattern when canonical and sampled disagree - ask. +- Never restore scaffold anchor comments without explicit consent. +- Never modify `composer.json`, `package.json`, or any lockfile beyond what the engine wrote. + +**Detect-and-correct:** if you notice an earlier artifact in the wrong directory (e.g. `includes/...` from unpassed conventions) or missing its test file, stop new work, migrate to the canonical layout (§3), add the missing tests, then resume. + +## Engine errors + +| Code | Response | +|---|---| +| `ENOSCAFFOLD` | Surface `available` list, suggest closest, ask. | +| `EMISSINGINPUT` | Read `missingDetails`, run §2 discovery, retry with resolved values. | +| `EBADSCAFFOLD` | Invalid manifest. Surface verbatim, do not retry. For an `origin: "remote"` scaffold this means the fetched manifest at its pinned ref is broken - surface it; do not try to repair another repo's scaffold. | +| `EWRITEFAIL` | Surface path + errno. Do not retry. | +| `ERENDERFAIL` | Scaffold author bug. Surface. | +| `EFETCHFAIL` | Network/HTTP failure fetching an `origin: "remote"` scaffold's manifest or a template. Surface `url` + `statusCode`. If the payload sets `rateLimited`, tell the developer to set `WP_TOOLING_GITHUB_TOKEN` and stop. A timeout is transient - one retry is reasonable; a 404 means the source pin (`sources.json` repo/ref/path) or the owning repo's index is wrong - surface, do not retry. Never hand-write the artifact to work around a failed fetch (the engine owns it). | +| Unknown | Surface, exit non-zero, do not crash. | + +## CI/CD variant + +- `ai.wiring` usually empty. +- `developer.secrets` usually populated. For multi-workflow setups, emit one consolidated `gh secret set` checklist at the end (dedupe). +- `ai.tests` framework is `actionlint` or `yaml-parse`. Validate; do not fill the YAML. + +## Reference + +- Engine contract: `node_modules/@rtcamp/wp-tooling/docs/ai-orchestration.md` +- Examples: `node_modules/@rtcamp/wp-tooling/docs/examples.md` +- Engine source: `node_modules/@rtcamp/wp-tooling/src/scaffolds/` +- Test templates: `node_modules/@rtcamp/wp-tooling/scaffolds/wp//templates/test.php.mustache` diff --git a/.claude/skills/scaffold/evals/evals.json b/.claude/skills/scaffold/evals/evals.json new file mode 100644 index 0000000..613da9d --- /dev/null +++ b/.claude/skills/scaffold/evals/evals.json @@ -0,0 +1,49 @@ +{ + "skill_name": "scaffold", + "evals": [ + { + "id": 1, + "prompt": "I need a WP-CLI command in my plugin that exports Query Monitor data as JSON. The plugin uses Acme\\Blog\\ -> src/ PSR-4 autoload. src/Plugin/Main.php has a register() method wiring commands via $this->commandRegistry->add(new \\Acme\\Blog\\CLI\\X()). Existing commands in src/CLI/ end with Command suffix (ImportCommand, PurgeCommand). Scaffold the qm-export command following these conventions.", + "expected_output": "A new PHP class under src/CLI/QmExportCommand.php with the project's Command suffix and Acme\\Blog\\CLI namespace, plus a PHPUnit test stub. Wiring inserted into src/Plugin/Main.php using the project's translated commandRegistry->add() pattern (not the canonical WP_CLI::add_command). No composer require executed.", + "files": [], + "expectations": [ + "The skill picks the wp/cli scaffold (not a guess across multiple candidates).", + "The skill introspects the project before invoking the engine and reports the four facts: namespace Acme\\Blog, base path src, Command suffix, commandRegistry->add() registration pattern.", + "The engine invocation passes --class=QmExportCommand (project's Command suffix), not the canonical bare QmExport.", + "The engine invocation passes --namespace=Acme\\Blog\\CLI (the sub-namespace sampled from the project).", + "The engine writes the PHP class file and a PHPUnit test stub.", + "The wiring inserted into src/Plugin/Main.php uses the project's commandRegistry->add() pattern, NOT the canonical WP_CLI::add_command() snippet.", + "Wiring is shown with an explicit consent prompt before applying.", + "The skill does NOT run composer require, composer install, or npm install on the user's behalf." + ] + }, + { + "id": 2, + "prompt": "Set up the GitHub Actions workflow that auto-deploys this WordPress plugin to wordpress.org SVN on each release. The plugin slug is acme-blog. The repo has package.json and composer.json but no .github/workflows directory yet.", + "expected_output": ".github/workflows/cd-wporg.yml written by the engine. The skill surfaces WPORG_USERNAME and WPORG_PASSWORD as a `gh secret set` checklist for the user. No secret values are invented or echoed. No `gh secret set` is executed automatically. actionlint (or yaml-parse) validation result reported.", + "files": [], + "expectations": [ + "The skill picks the ci/cd-wporg scaffold.", + "The engine invocation passes --plugin-slug=acme-blog.", + "The engine writes .github/workflows/cd-wporg.yml (and the actionlint test entry references the same file).", + "Both WPORG_USERNAME and WPORG_PASSWORD are surfaced as a `gh secret set` checklist for the user to run themselves.", + "The skill does NOT invent secret VALUES (no fake passwords, no 'changeme' placeholders presented as if real).", + "The skill does NOT execute `gh secret set` itself.", + "The skill reports the actionlint (or yaml-parse fallback) validation result for the generated YAML." + ] + }, + { + "id": 3, + "prompt": "Add a dynamic Gutenberg block called pricing-table to my project. Blocks live at src/blocks/ and we use a vendor prefix of 'acme' for block.json names (existing blocks look like acme/hero, acme/cta).", + "expected_output": "src/blocks/pricing-table/ created with block.json (name: acme/pricing-table), edit.js, index.js, render.php. The vendor prefix 'acme' is detected from an existing block.json and applied. No npm install or wp-scripts build executed automatically.", + "files": [], + "expectations": [ + "The skill picks the wp/block-dynamic scaffold.", + "The skill introspects an existing src/blocks/*/block.json to confirm the 'acme' vendor prefix and the src/blocks/ source directory.", + "The engine invocation passes a vendor/namespace input that produces 'acme/pricing-table' in the generated block.json (not 'wp-tooling/pricing-table' or unprefixed 'pricing-table').", + "The engine writes block.json, edit.js, index.js, and render.php under src/blocks/pricing-table/.", + "The skill does NOT run npm install or wp-scripts build." + ] + } + ] +} diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md new file mode 100644 index 0000000..055215f --- /dev/null +++ b/.claude/skills/setup/SKILL.md @@ -0,0 +1,388 @@ +--- +name: setup +description: Bootstrap a WordPress plugin or theme from a natural-language description. Detects project type, applies tooling (EditorConfig, PSR-4, PHPCS, PHPStan, ESLint, Stylelint, PHPUnit, Jest, pa11y), and chains feature scaffolds (CLI commands, blocks, REST, etc.) in one session. Asks for clarification whenever intent is ambiguous - never assumes. +--- + +# setup + +You are configuring a WordPress plugin or theme from a natural-language request. Your job is to understand exactly what the developer wants, turn that into a sequenced plan of scaffold invocations, confirm the plan, and execute it. You never assume anything you cannot verify by reading the project directory. + +This skill is meant to be **copied verbatim** into the user's repo at `.claude/commands/setup.md` or `.claude/skills/setup.md`. It assumes `@rtcamp/wp-tooling` is on the `PATH` (via `npx`) or installed as a project dev dependency. + +## When to use this skill + +Use when the developer asks to: + +- "Set up my plugin/theme" or "scaffold this as a [description]." +- Bootstrap a new empty project directory. +- Add rtCamp standard tooling to an existing project. +- Create a feature scaffold (CLI command, block, REST controller, etc.) as part of a setup session. + +Do not use this skill for: isolated bug fixes, one-off file edits, or anything outside the `@rtcamp/wp-tooling` scaffold catalogue. + +## Workflow + +### 0. Parse the request + +Read the developer's message carefully. Extract: + +- **Project type**: plugin vs theme. +- **Standards**: VIP vs non-VIP vs WordPress.org plugin vs plain WordPress. +- **Languages needed**: PHP, JS (blocks, scripts), CSS/SCSS. +- **Features wanted**: CLI command, REST endpoint, cron job, block, post type, integrations, etc. +- **Tests wanted**: PHPUnit (PHP unit/integration), Jest (JS), pa11y (a11y), or none. +- **Any specific names, namespaces, or requirements** mentioned by the developer. + +If the request is vague or any of the above is unclear, **stop and ask before reading the project directory**. Do not try to infer project type from the directory if the developer's message already says it. Do not proceed with gaps. + +Example clarifying questions (ask all at once, never drip): + +``` +Before I start, I need a few details: + +1. Is this a WordPress plugin or a theme? +2. Will this be deployed on WordPress VIP? (Determines the PHPCS standard.) +3. What is the PHP root namespace you want to use? (e.g. Acme\\ImageOptimizer) +4. What directory holds the PHP source? (e.g. includes/ or src/) +5. Do you want tests set up? If so: PHPUnit for PHP, Jest for JS, pa11y for a11y - any or all? +6. You mentioned a CLI command - what should it do? (I need a command name or slug.) +``` + +### 1. Detect what already exists + +After the request is clear, read the project directory to avoid duplicating work. + +**Project type (verify against developer's description):** + +```bash +grep -rl "Plugin Name:\|Theme Name:" . --include="*.php" --exclude-dir=vendor --exclude-dir=node_modules -l | head -1 +``` + +**VIP indicators:** + +```bash +grep -rl "WordPress-VIP-Minimum\|automattic/vip-coding-standards\|VIP_GO_ENV" . \ + --include="*.{php,xml,json,yml}" --exclude-dir=vendor --exclude-dir=node_modules | head -3 +``` + +**Languages present:** + +```bash +find . -name "*.php" -not -path "*/vendor/*" | head -1 +find . \( -name "*.js" -o -name "*.jsx" \) -not -path "*/node_modules/*" -not -path "*/build/*" | head -1 +find . \( -name "*.scss" -o -name "*.css" \) -not -path "*/node_modules/*" | head -1 +``` + +**PSR-4 autoload:** + +```bash +grep -A 8 '"autoload"' composer.json 2>/dev/null +``` + +**Existing tooling configs (skip if present):** + +```bash +ls .editorconfig phpcs.xml.dist phpstan.neon.dist eslint.config.js .stylelintrc.js \ + phpunit.xml.dist jest.config.js .pa11yci.json 2>/dev/null +``` + +**Existing namespace (if PSR-4 missing):** + +```bash +grep -r "^namespace " . --include="*.php" --exclude-dir=vendor | head -3 +``` + +If detection contradicts what the developer said, flag it and ask. Never silently override the developer's stated intent with what you find on disk. + +### 2. Build the scaffold plan + +Construct the plan in two phases: **project setup** and **feature scaffolds**. + +#### Phase A: Project setup + +| Condition | Scaffold | Skip if | +|---|---|---| +| Always | `setup/editorconfig` | `.editorconfig` exists | +| PHP present, no PSR-4 in `composer.json` | `setup/psr4` | `autoload.psr-4` already set | +| VIP project | `lint/phpcs/vip` | `phpcs.xml.dist` exists | +| Non-VIP project | `lint/phpcs/full` | `phpcs.xml.dist` exists | +| Developer explicitly chose core-only PHPCS | `lint/phpcs/core` | `phpcs.xml.dist` exists | +| PHP present | `lint/phpstan` | `phpstan.neon.dist` exists | +| JS present | `lint/eslint` | `eslint.config.js` exists | +| CSS or SCSS present | `lint/stylelint` | `.stylelintrc.js` exists | +| Developer wants PHP tests | `setup/phpunit` | `phpunit.xml.dist` exists | +| Developer wants JS tests | `setup/jest` | `jest.config.js` exists | +| Developer wants a11y tests | `setup/pa11y` | `.pa11yci.json` exists | + +#### Phase B: Feature scaffolds + +Map each feature the developer mentioned to one or more scaffold IDs from the catalogue: + +| Developer said | Scaffold ID | +|---|---| +| CLI command | `wp/cli` | +| REST endpoint / API | `wp/rest` (if available) | +| Cron job / background job | `wp/cron` (if available) | +| Gutenberg block | `wp/block-dynamic` | +| Cache / transients | `utility/cache` | +| CI pipeline | `ci/cd-wporg` (or other CI scaffold) | + +Run `npx wp-tooling list --json` to see exactly what is available. If a feature the developer wants has no matching scaffold, note it explicitly as a manual task in the final report. + +For each feature scaffold, you need the same project-convention information as the `scaffold` skill requires (namespace, base path, class suffix, registration pattern). Collect this once from the project and cache it. + +### 3. Confirm the full plan before doing anything + +Show the complete two-phase plan. Be specific: include the scaffold ID, what file(s) it writes, and any inputs it will use. + +``` +Here is what I will do. Please confirm or adjust before I start. + +Phase A - Project setup: + 1. setup/editorconfig → .editorconfig + 2. setup/psr4 → wiring in composer.json (namespace: Acme\ImageOptimizer, path: includes/) + 3. lint/phpcs/vip → phpcs.xml.dist (WordPress-VIP-Minimum + WordPress-Docs) + 4. lint/phpstan → phpstan.neon.dist (extends rtCamp wp-phpstan baseline) + 5. lint/eslint → eslint.config.js + 6. setup/phpunit → phpunit.xml.dist, tests/bootstrap.php + +Phase B - Feature scaffolds: + 7. wp/cli → includes/Cli/OptimizeImagesCommand.php + (namespace: Acme\ImageOptimizer\Cli, class suffix: Command, + registers via $this->boot(...) in includes/Plugin.php) + +Skipped (already present): none. + +Not in catalogue (manual tasks): none. + +Confirm? Or adjust (e.g. "remove phpunit", "use full phpcs instead of vip", "add jest")? +``` + +Do not start running scaffolds until the developer confirms. If they adjust, update the plan and confirm once more before starting. + +### 4. Execute Phase A + +Run each setup scaffold in order. Use `--non-interactive --json --cwd .`. + +For `setup/editorconfig`: + +```bash +npx wp-tooling add setup/editorconfig --non-interactive --json --cwd . +``` + +For `setup/psr4` (use detected or developer-supplied namespace and base path): + +```bash +npx wp-tooling add setup/psr4 \ + --non-interactive --json --cwd . \ + --namespace='Acme\ImageOptimizer' --base-path='includes' +``` + +For lint and test setup scaffolds: + +```bash +npx wp-tooling add lint/phpcs/vip --non-interactive --json --cwd . +npx wp-tooling add lint/phpstan --non-interactive --json --cwd . +npx wp-tooling add lint/eslint --non-interactive --json --cwd . +npx wp-tooling add setup/phpunit --non-interactive --json --cwd . --source-dir=includes +npx wp-tooling add setup/pa11y --non-interactive --json --cwd . --base-url=http://localhost:8888 +``` + +Process each result before running the next: + +- Report files written and skipped. +- Apply `setup/psr4` wiring to `composer.json` with explicit consent (see §Wiring below). +- Accumulate `developer.install.*` and `developer.scripts.*` across all scaffolds. + +### 5. Execute Phase B + +For each feature scaffold, follow the full workflow from `skills/scaffold.md` - introspect conventions, apply naming, invoke the engine, adaptive wiring, **expand test stubs into a real suite, then drive implementation from those tests** (red → green → refactor). + +Do not batch feature scaffolds. Run one at a time, apply its wiring, complete the TDD loop (see `skills/scaffold.md` §5b), then move to the next. + +For `wp/cli`, pass the same project conventions detected in Stage 1 (namespace, base path, class suffix) - defaults assume the rtCamp skeleton (`Inc\Cli`, `includes/Cli`) and will be wrong for any other project: + +```bash +npx wp-tooling add wp/cli \ + --non-interactive --json --cwd . \ + --namespace='Acme\ImageOptimizer\Cli' --base-path='includes/Cli' \ + --name=optimize-images --class=OptimizeImagesCommand +``` + +The engine emits `ai.wiring` (where to register the command) and a thin test stub at `tests/Cli/OptimizeImagesCommandTest.php`. Show the wiring snippet, get consent, apply. Then turn the stub into a real test suite (happy path, dry-run flag, edge cases, error handling) and implement `__invoke()` test-by-test until the suite is green. Never leave `markTestIncomplete` in the final state. + +**Code quality (required).** For any PHP a feature scaffold writes or changes under `inc/` or `tests/`, run the compliance pipeline once the implementation is green, exactly as in the `scaffold` skill (`skills/scaffold.md` §7a): `composer format` (phpcbf) -> `composer lint` (phpcs, `rtCampWP` ruleset via `phpcs.xml.dist`) -> `composer phpstan` (PHPStan level 5 via `phpstan.neon.dist`), resolving every finding in the generated code. Write it compliant in the first place (`declare( strict_types = 1 );`, short arrays, typed signatures, prefixed globals, documented). If a fix is unclear or would change behaviour, public API, or intent, STOP and ask - never silence a real issue with a blanket `phpcs:ignore` / `@phpstan-ignore`. + +**Phase B feature scaffolds require the matching test framework from Phase A.** If Phase A skipped `setup/phpunit` because the developer did not ask for tests, surface this before running Phase B feature scaffolds: + +``` +You asked for a CLI command, but Phase A skipped setup/phpunit. +The wp/cli scaffold ships a PHPUnit stub I cannot run without it. + +Options: + 1. Add setup/phpunit now (recommended - I drive feature development from tests). + 2. Proceed without tests (stub will be written but not executed; I will note this as a manual follow-up). + +Which? +``` + +Default to (1). Only proceed without tests if the developer explicitly chooses (2). + +### Wiring: composer.json PSR-4 + +When `setup/psr4` wiring is received: + +1. Show the current `"autoload"` block in `composer.json` (or note it is absent). +2. Show the intended entry: namespace `Acme\ImageOptimizer` maps to `includes/`. +3. Ask: `Apply PSR-4 autoload to composer.json? [apply / skip]` +4. If apply: paste the engine's `ai.wiring[0].snippet` verbatim. The engine already emits the PSR-4 key JSON-encoded with its trailing backslash (e.g. `"Acme\\ImageOptimizer\\"`) - **do not escape it again**, or you will double the backslashes and break autoload. +5. Remind: run `composer dump-autoload --optimize` after applying. + +If `composer.json` does not exist, offer to create a minimal one: + +``` +composer.json not found. I can create a minimal one: + + { + "name": "vendor/image-optimizer", + "type": "wordpress-plugin", + "require": { "php": ">=8.3" }, + "autoload": { + "psr-4": { "Acme\\ImageOptimizer\\": "includes/" } + } + } + +Create it? [yes / skip PSR-4 / give me the values to use] +``` + +### Wiring: feature scaffolds + +Same as `skills/scaffold.md` §5 - `ai.wiring`. Show diff, get consent, apply. + +### 6. Consolidated final report + +``` +Setup complete. + +Files written (Phase A): + .editorconfig + phpcs.xml.dist (WordPress VIP Minimum + Docs) + phpstan.neon.dist (extends rtCamp wp-phpstan baseline) + eslint.config.js + phpunit.xml.dist + tests/bootstrap.php + composer.json (PSR-4 autoload added, Acme\ImageOptimizer → includes/) + +Files written (Phase B): + includes/Cli/OptimizeImagesCommand.php + tests/Cli/OptimizeImagesCommandTest.php + +Wiring applied: + includes/Plugin.php:43 - $this->boot('optimize-images', \Acme\ImageOptimizer\Cli\OptimizeImagesCommand::class); + +Tests: + tests/Cli/OptimizeImagesCommandTest.php - stub passing. + +Developer actions (run these yourself): + + composer dump-autoload --optimize + + composer require --dev \ + automattic/vip-coding-standards:^3.0 \ + wp-coding-standards/wpcs:^3.0 \ + squizlabs/php_codesniffer:^3.7 \ + phpunit/phpunit:^12.0 \ + yoast/phpunit-polyfills:^4.0 \ + brain/monkey:^2.6 + + npm install --save-dev \ + eslint@^10.0.0 \ + @wordpress/eslint-plugin@^25.1.0 \ + @rtcamp/eslint-config@^0.1.0 + +Scripts to add to composer.json: + + "scripts": { + "lint:php": "phpcs --standard=phpcs.xml.dist", + "lint:php:fix": "phpcbf --standard=phpcs.xml.dist", + "phpstan": "phpstan analyse", + "test": "phpunit", + "test:unit": "phpunit --testsuite Unit", + "test:integration": "phpunit --testsuite Integration" + } + +Scripts to add to package.json: + + "scripts": { + "lint:js": "eslint 'src/**/*.js' 'assets/**/*.js'", + "lint:js:fix": "eslint 'src/**/*.js' 'assets/**/*.js' --fix" + } + +Skipped: none. +Outstanding manual tasks: none. +``` + +Deduplicate packages. Sort alphabetically within each block. Pinned packages use exact versions; everything else uses range specifiers. + +## PHPCS standard reference + +| Scaffold ID | When to use | Ruleset | +|---|---|---| +| `lint/phpcs/full` | Most rtCamp projects (recommended default) | `rtCampWP` from `rtcamp/wp-phpcs`: WordPress-Extra + Docs + VIP-Go + PHPCompatibilityWP, plus strict types, type hints and short arrays | +| `lint/phpcs/vip` | WordPress VIP platform projects | `WordPress-VIP-Minimum` + `WordPress-Docs` | +| `lint/phpcs/core` | Projects explicitly opting out of VIP-Go rules | `WordPress` - Core + Extra + Docs only | + +Developers can add `` entries to `phpcs.xml.dist` to override or extend the selected standard. + +## Test scaffolds reference + +| Scaffold ID | What it installs | When to apply | +|---|---|---| +| `setup/phpunit` | `phpunit.xml.dist`, `tests/bootstrap.php`, PHPUnit + polyfills + Brain Monkey | PHP plugin or theme with tests | +| `setup/jest` | `jest.config.js`, `@wordpress/jest-preset-default` | JS blocks or scripts with unit tests | +| `setup/pa11y` | `.pa11yci.json`, `pa11y-ci` | Any project needing WCAG2AA accessibility coverage | + +## Rules: never assume, always ask + +Before running any scaffold, every required input must be confirmed by the developer or verified from the project files. The following facts require explicit confirmation or verification - never infer them silently: + +- Plugin vs theme. +- VIP vs non-VIP. +- PHP root namespace and base directory. +- CLI command slug and class name. +- Block slug and vendor prefix. +- REST route and controller name. +- pa11y base URL. + +If the developer's request is clear enough that a fact can be read unambiguously from the project (e.g. namespace from `composer.json` autoload, VIP from existing `phpcs.xml.dist`), no need to ask - cite the source in the confirmation plan instead. + +## Hard prohibitions + +You **must never** (BASE, see AGENTS.md guardrails): + +- Run history/remote `git`/`gh` at all (commit, push, `branch -D`, `reset --hard`, PR, issue comment, `gh secret set`); print them as developer actions. `git clone`/`checkout` for setup are fine. +- Do a destructive operation outside this plugin directory (cloning a NEW sibling is additive and OK; deleting/overwriting existing out-of-repo files is not). +- Commit or push `graphify-out/graph.json`; local graph refreshes stay local. +- Run `composer require`, `npm install`, `composer dump-autoload`, or any package manager command without explicit user approval. +- Edit `composer.json` scripts or `package.json` scripts - show them, let the developer apply. +- Apply wiring to any file without showing the diff and receiving consent. +- Apply more scaffolds than the confirmed plan. +- Silently skip a scaffold; always report skips with a reason. +- Set up CI/CD - that requires a separate `scaffold` skill invocation targeting `ci/` scaffolds. +- Declare generated PHP done without running the §5 code-quality pipeline (`composer format` -> `composer lint` -> `composer phpstan`) clean, or silence a real finding with a blanket `phpcs:ignore` / `@phpstan-ignore` to pass it. +- Commit, push, or open PRs without explicit approval. + +## Error handling + +For `ENOSCAFFOLD`: The requested scaffold id does not exist. Surface the `available` list, show the closest match, and ask the developer what to do. + +For `EMISSINGINPUT`: Read `missingDetails`, collect the values from the project or ask the developer, and retry with the resolved values. + +For `EWRITEFAIL`: Surface the path and OS error. Ask whether to retry (e.g. after the developer fixes permissions) or skip the file. + +For `EBADSCAFFOLD`: Scaffold author bug. Surface verbatim. Do not retry. + +## Reference + +- Worked conversation examples: `node_modules/@rtcamp/wp-tooling/docs/examples.md` (greenfield plugin, VIP image-optimizer setup, all phases in one prompt). +- One-off scaffold additions are handled by the companion `scaffold` skill. diff --git a/.claude/skills/setup/evals/evals.json b/.claude/skills/setup/evals/evals.json new file mode 100644 index 0000000..729012a --- /dev/null +++ b/.claude/skills/setup/evals/evals.json @@ -0,0 +1,49 @@ +{ + "skill_name": "setup", + "evals": [ + { + "id": 1, + "prompt": "Set up a new WordPress VIP plugin called My Plugin in the Acme\\ImageOptimizer namespace under includes/. I want PHPCS, PHPStan, and PHPUnit. Then add a WP-CLI command called optimize-images that I'll fill in later.", + "expected_output": "Two-phase plan executed across the greenfield project: Phase A installs editorconfig + psr4 + phpcs/vip + phpstan + phpunit (skipping eslint/stylelint/jest/pa11y with reasons), Phase B scaffolds the wp/cli command. A minimal composer.json is offered when the file doesn't exist. One consolidated final report covers files written, dev-deps to install, and outstanding manual tasks. No composer require / composer dump-autoload / npm install / gh secret set executed.", + "files": [], + "expectations": [ + "Stage 0: The skill parses the request and identifies all stated facts (plugin, VIP=yes, namespace, base path, features, tests). Does NOT ask clarifying questions for facts already stated.", + "Stage 1: Detects the project is greenfield (no tooling configs, no composer.json autoload).", + "Stage 2: Plan includes setup/editorconfig + setup/psr4 + lint/phpcs/vip + lint/phpstan + setup/phpunit for Phase A, and wp/cli for Phase B.", + "Stage 2: Skips lint/eslint and lint/stylelint with reasons (no JS or CSS detected). Skips setup/jest and setup/pa11y with reasons (not requested).", + "Stage 3: Shows the full plan with scaffold ids + files each will write + inputs + skipped-with-reasons, AND waits for confirmation before executing.", + "Stage 4: Executes scaffolds one at a time. For setup/psr4 when no composer.json exists, offers the minimal-composer.json template with the JSON-escaped namespace key 'Acme\\\\ImageOptimizer\\\\'.", + "Stage 5: Defaults to adding setup/phpunit before running wp/cli if it wasn't already in Phase A (Phase B test-driven scaffolds need their framework).", + "Stage 6: Emits one consolidated final report — files written across both phases, dedup'd `composer require --dev` lines (using composerDev field), scripts to add to manifests, skipped-with-reasons, outstanding manual tasks.", + "The skill does NOT run composer require, composer dump-autoload, npm install, or gh secret set on the user's behalf." + ] + }, + { + "id": 2, + "prompt": "Bootstrap this thing for me", + "expected_output": "Recognises the request is ambiguous and stops to ask clarifying questions before reading the project directory. Questions are batched (not drip-fed) and cover at minimum: plugin vs theme, VIP vs non-VIP, namespace + base path, which tests are wanted. No scaffolds executed before the developer provides clarification.", + "files": [], + "expectations": [ + "The skill recognises the request is ambiguous and STOPS before reading the project directory.", + "The skill asks clarifying questions in ONE batch (not drip-fed across multiple turns), covering at minimum: plugin vs theme, VIP vs non-VIP, namespace + base path, which tests are wanted.", + "The skill does NOT attempt to infer project type from disk before asking — the developer's message is silent on the basics, and 'ask first' is the rule.", + "The skill does NOT execute any scaffolds before the developer provides clarification." + ] + }, + { + "id": 3, + "prompt": "Add rtCamp tooling to my existing plugin. It's at the repo root. composer.json already declares Acme\\Blog\\ -> src/ PSR-4 autoload and there's already a phpcs.xml.dist with WordPress-VIP-Minimum. Add PHPStan and PHPUnit; I don't need JS lint or a11y.", + "expected_output": "Detection cites the existing composer.json autoload and phpcs.xml.dist. setup/psr4 and lint/phpcs/vip skipped with reasons. Plan includes setup/editorconfig (if missing), lint/phpstan, setup/phpunit. lint/eslint, lint/stylelint, setup/jest, setup/pa11y NOT included because the developer ruled them out. Plan confirmed with developer before executing. Final report lists skipped scaffolds with the reason for each.", + "files": [], + "expectations": [ + "The skill detects the existing composer.json autoload (does not ask for namespace) and the existing phpcs.xml.dist (VIP confirmed without asking).", + "Both findings are cited in the confirmation plan with their source (composer.json, phpcs.xml.dist), not silently used.", + "The plan skips setup/psr4 (already configured) and lint/phpcs/vip (config exists) — both reported as skipped with reasons.", + "The plan includes only setup/editorconfig (if missing), lint/phpstan, setup/phpunit.", + "The plan does NOT include lint/eslint, lint/stylelint, setup/jest, or setup/pa11y — developer explicitly ruled them out.", + "The skill confirms the plan with the developer before executing.", + "The final report explicitly lists skipped scaffolds with the reason for each." + ] + } + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f72a2e3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab + +[{.babelrc,.eslintrc,.rtlcssrc,*.json,*.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..76efcd5 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,28 @@ +# Local site hostname (without protocol or trailing slash). +# LocalWP: yoursite.local, Lando: yoursite.lndo.site, wp-env: localhost, WP Studio: localhost +WP_HOST=yoursite.local + +# HMR / BrowserSync live reload master switch (default: on). Honoured by both +# the build (BrowserSync server) and PHP (client enqueue). Set to false to fully +# disable live reload. Off values: 0, false, no, off. Toggle from `npm run init`. +ENABLE_HMR=true + +# BrowserSync port. Default: 3003. Change if port 3003 is already in use. +# Both the build and PHP read this value directly, so changing it here is +# enough. Define ELEMENTARY_PLUGIN_BROWSER_SYNC_URL only to override the +# full client URL (e.g. remote/proxy setups), not just the port. +BS_PORT=3003 + +# Block HMR dev-server port (used by `npm run start:blocks`). +# Default: 8888. Change if port 8888 is already in use. +BLOCKS_DEV_SERVER_PORT=8888 + +# Disable BrowserSync client enqueue. Truthy: 1, true, yes, on (default off). +# The BrowserSync server still runs; only the frontend client is skipped. +# DISABLE_BS=true + +# SSL cert paths — only needed when your local site runs on HTTPS. +# LocalWP stores certs at: +# macOS: ~/Library/Application Support/Local/run/router/nginx/certs/.key / .crt +WP_SSL_KEY=/path/to/yoursite.local.key +WP_SSL_CERT=/path/to/yoursite.local.crt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 243ce7b..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @Adi-ty \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..4bc8afb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,24 @@ +--- +name: Bug Report +about: There is a bug in functionality that needs to be fixed. +title: '' +labels: '' +assignees: '' + +--- + +## Description + + + +## Steps to Reproduce + + + +## Screenshots + + + +## Additional Information + + diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..c55b6e9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,50 @@ +name: Bug Report Form +description: If something isn't working as expected +labels: + - bug +body: + - type: textarea + attributes: + label: Current Behaviour + description: A clear and concise description of the buggy behavior. + validations: + required: true + - type: textarea + attributes: + label: Expected Behaviour + description: >- + A clear and concise description of what you expected to happen (or + code). + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce the bug + description: Steps to reproduce if any. + placeholder: Steps to reproduce bug. + value: |- + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + attributes: + label: Testing Environment (if applicable) + description: Steps to reproduce if any. + validations: + required: true + - type: textarea + attributes: + label: Additional context/Screenshots + description: >- + Add any other context about the problem here. If applicable, add + screenshots to help explain. + validations: + required: true + - type: input + attributes: + label: Estimated time in hours + description: Enter the estimated time required to finish this task. + placeholder: 40 Hours diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 0000000..9ea3baa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,25 @@ +name: Enhancement +description: template for missing feature +title: '[Enhancement]: ' +labels: + - enhancement +body: + - type: textarea + attributes: + label: Description + description: A clear and concise description of the problem or missing capability + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: If you have a solution in mind, please describe it. + - type: textarea + attributes: + label: Describe alternatives you've considered + description: Have you considered any alternative solutions or workarounds? + - type: input + attributes: + label: Estimated time in hours + description: Enter the estimated time required to finish this task. + placeholder: 40 Hours diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md new file mode 100644 index 0000000..a1fdc67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -0,0 +1,24 @@ +--- +name: Epic +about: An Epic for tracking multiple tasks related to a theme of work. +title: '' +labels: '' +assignees: '' +--- + +## Summary + + + +## References + + + +## Tasks + + diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..789e1df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,25 @@ +name: Feature Request +description: template for missing feature +title: '[Feature Request]: ' +labels: + - feature +body: + - type: textarea + attributes: + label: Description + description: A clear and concise description of the problem or missing capability + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: If you have a solution in mind, please describe it. + - type: textarea + attributes: + label: Describe alternatives you've considered + description: Have you considered any alternative solutions or workarounds? + - type: input + attributes: + label: Estimated time in hours + description: Enter the estimated time required to finish this task. + placeholder: 40 Hours diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..7956bff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,24 @@ +--- +name: Task +about: A descriptive task to be done. +title: '' +labels: '' +assignees: '' +--- + +## Summary + + + +## References + + + +## Acceptance Criteria + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1a3d51b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +## Description + + + +## Technical Details + + + +## Checklist + + + +## Screenshots + + + +## To-do + + + +## Fixes/Covers issue + + +Fixes # + + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..4e953aa --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,27 @@ +# Copilot instructions: Plugin Elementary + +Read **[AGENTS.md](../AGENTS.md)** first: it is the source of truth for this project's conventions (stack, structure, TDD, framework patterns, security, guardrails), shared across all AI tools. This file adds only the Copilot-specific review conduct. + +Detailed, path-scoped rules live in [`.github/instructions/`](instructions/): +- `primitives-php.instructions.md` (generated by `npm run sync-ai` from the framework; absent until then): `**/*.php` framework architecture, security, testing, review flags. +- `structure.instructions.md`: plugin layout and wiring. + +This is the Plugin Elementary template: names are placeholders (`rtCamp\Plugin\Elementary` / `elementary-plugin`) filled in per project. The framework (`rtCamp\WPPrimitives`) lives in the gitignored `vendor/` and is not visible at review. + +## Task prompts + +Two reusable prompts in [`prompts/`](prompts/), invoked in Copilot Chat. They are the Copilot peers of the Claude skills, kept consistent with them: +- `/init` ([init.prompt.md](prompts/init.prompt.md)): set up / manage the project; installs dependencies (`npm install`, `composer install`), then drives `npm run init`. +- `/scaffold` ([scaffold.prompt.md](prompts/scaffold.prompt.md)): add one feature, TDD-first, via `@rtcamp/wp-tooling`. + +Both are agentic: gather inputs and confirm before acting, and after any code change refresh the local knowledge graph (`graphify update .`, never committed) per the graphify policy in [AGENTS.md](../AGENTS.md). The review conduct below applies to PR review, not to running these prompts. + +## Review conduct + +These tune *how* you comment; the *what* lives in AGENTS.md and the path-scoped files. Balance matters: catch every real issue, but don't drown it in noise. + +- **One comment per distinct issue, and never more than one per comment.** Every distinct problem gets its OWN separate comment at its own line, even when several issues sit on the same or adjacent lines (e.g. an unauthenticated `permission_callback`, raw `$_POST` use, and SQL injection in one method are THREE comments, not one). Do not merge unrelated findings. Conversely, when a *single* root cause spans several lines (e.g. the `Singleton` trait plus its `get_instance()` bootstrap), post it once at the clearest line and name the related lines; don't repeat the same finding. +- **Always post the missing-tests (TDD) finding** when a new/changed feature or class ships without a matching test in `tests/php/`. This is mandatory and exempt from dedupe, trimming, and any comment-budget; never drop it in favour of other findings. +- **Order the rest by impact:** security → architecture/contract (`Singleton`/`Shareable`/`Loader`/PSR-4/abstracts) → WordPress correctness → style. Lead with what breaks things. +- **Don't spend comments on what PHPCS/PHPStan already enforce.** A bare "add a `void` return type" or "add visibility" is noise; the linters catch it. Only raise a typing issue when it changes behavior or hides a real bug. If a non-linter-covered style note is worth raising, give it its own comment too. +- **On a correct, deliberate implementation, don't manufacture findings.** An idiomatic, defensible choice is not a bug. Prefer **zero comments** over low-value ones. A clean PR comes back clean. (A genuine correctness/security observation on otherwise-good code is still welcome; a stylistic preference is not.) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6a6bb14 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + open-pull-requests-limit: 10 + labels: + - Dependencies + + - package-ecosystem: npm + directory: '/' + schedule: + interval: weekly + open-pull-requests-limit: 10 + versioning-strategy: increase + labels: + - Dependencies + + - package-ecosystem: composer + directory: '/' + schedule: + interval: weekly + open-pull-requests-limit: 10 + versioning-strategy: increase + labels: + - Dependencies diff --git a/.github/instructions/primitives-php.instructions.md b/.github/instructions/primitives-php.instructions.md new file mode 100644 index 0000000..fbd1357 --- /dev/null +++ b/.github/instructions/primitives-php.instructions.md @@ -0,0 +1,56 @@ +--- +applyTo: "**/*.php" +description: "Framework + WordPress rules for PHP. Canonical source, shipped by rtcamp/wp-primitives." +--- + +# PHP rules: framework, WordPress, security, tests + +`rtcamp/wp-primitives` (`rtCamp\WPPrimitives`) is in gitignored `vendor/` (invisible at review). These rules ARE its contract. + +## Architecture + +`Main` (`use Singleton; use Loader;`) lists classes in `const CLASSES`. The `Loader` instantiates each → if `Registrable`, calls `register_hooks()` (skipped when `ConditionallyRegistrable::can_register()` is false) → if `Shareable`, caches it in a `Container` (fetch via `get_shared()`). Otherwise a fresh, non-shared instance. + +Decision order for a new class, **do NOT default to Singleton**: +1. **plain `Registrable`**: default; loaded by the `Loader`, never retrieved elsewhere. +2. **`Registrable` + `Shareable`**: only if another class must retrieve it via `get_shared()`. +3. **`Singleton`**: only the `Main` bootstrap. + +Extend the framework abstracts; never hand-roll their job: `AbstractModule`, `AbstractFeature`, and `Abstract{PostType,Taxonomy,Block,Shortcode,RESTController,SettingsPage,AdminPage,UserRole,Ability,AbilityRegistrar}`. + +Flag genuine contract/security violations, not style. Allow any correct implementation. + +## Mandatory + +- **TDD**: failing PHPUnit test first (`tests/php/` mirrors `inc/`, extend the package `TestCase`), then code. +- `declare( strict_types = 1 );` at the top of every file. Full param + return types. `@package` + `@since` on every class/trait docblock. +- `snake_case` methods, `PascalCase` classes, filename === class, namespace === directory (PSR-4). +- `static::`, never `self::`, for late static binding. + +## WordPress security (always) + +- Escape on output (`esc_html`/`esc_attr`/`esc_url`/`wp_kses_post`); sanitize on input (`sanitize_text_field`/`absint`/`sanitize_key`). +- Verify a nonce **and** `current_user_can()` before any mutation (form/AJAX/REST). +- DB: `$wpdb->prepare()` with placeholders; never concatenate input. +- REST routes: always set a real `permission_callback` (never `__return_true` for writes); validate via the `args` schema. +- Use `===`/`!==`. + +## WordPress conventions + +- i18n: wrap user strings with the package text domain (`__()`, `esc_html__()`). +- Assets: `wp_enqueue_script/style` on the enqueue hooks, conditional per screen; never inline `