From 60ca7692497688ba240718bbf0eea9e56a81f4a3 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 24 Jun 2026 12:30:47 +0530 Subject: [PATCH 1/2] feat: add AI skills, Logger service, and graphify knowledge graph Port the AI-tooling layer from the features-plugin-skeleton, reworded for this theme: init/scaffold/setup skills + Copilot prompts, graphify scripts and committed graph, and an inc/Core/Logger service with Util accessors. --- .claude/skills/README.md | 18 + .claude/skills/init/SKILL.md | 140 + .claude/skills/scaffold/SKILL.md | 263 + .claude/skills/scaffold/evals/evals.json | 50 + .claude/skills/setup/SKILL.md | 363 + .claude/skills/setup/evals/evals.json | 49 + .../framework-php.instructions.md | 56 + .github/prompts/init.prompt.md | 98 + .github/prompts/scaffold.prompt.md | 77 + .gitignore | 7 +- AGENTS.md | 31 + CLAUDE.md | 18 +- README.md | 10 + bin/init.js | 38 +- composer.lock | 15 +- docs/knowledge-graph.md | 75 + graphify-out/GRAPH_REPORT.md | 284 + graphify-out/graph.json | 16051 ++++++++++++++++ inc/Core/Logger.php | 34 + inc/Helpers/Util.php | 104 +- inc/Main.php | 3 +- package.json | 2 +- 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 + tests/php/inc/Core/LoggerTest.php | 145 + 28 files changed, 18321 insertions(+), 55 deletions(-) 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 .github/instructions/framework-php.instructions.md create mode 100644 .github/prompts/init.prompt.md create mode 100644 .github/prompts/scaffold.prompt.md create mode 100644 docs/knowledge-graph.md create mode 100644 graphify-out/GRAPH_REPORT.md create mode 100644 graphify-out/graph.json create mode 100644 inc/Core/Logger.php 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 tests/php/inc/Core/LoggerTest.php diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 00000000..752e2cdc --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,18 @@ +# AI skills + +Skills for AI assistants (Claude Code, Cursor, and any tool that reads the Claude Code skill convention) that drive this theme'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 this cloned theme into a named theme and manage it afterwards: rename the starter tokens, keep/remove the example sets, toggle optional features (Tailwind, HMR). Drives `npm run init`. | +| [`scaffold/`](scaffold/SKILL.md) | `/scaffold` | Add one feature (dynamic block, shortcode, settings/admin page, service, CI workflow - plus CPT/taxonomy/REST/CLI/cron for a companion plugin) via `npx wp-tooling add`, TDD-first, wiring it into `Main::CLASSES`. | + +`init` is theme-specific. `scaffold` tracks `@rtcamp/wp-tooling` and introspects the project, so it stays correct as the layout evolves. [`setup/`](setup/SKILL.md) (`/setup`) is the natural-language bootstrapper: from one brief it applies tooling and chains a sequence of feature scaffolds. + +**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. + +Two consented exceptions to the package-manager rule: `npm run init` (the theme's own setup script), and the **pilot bootstrap** `init` runs on request (sibling clone of the tooling engine + local `file:` ref + `composer update`/`npm install`). After any code change, the skills refresh the local graph with `graphify update .`. diff --git a/.claude/skills/init/SKILL.md b/.claude/skills/init/SKILL.md new file mode 100644 index 00000000..21b6021d --- /dev/null +++ b/.claude/skills/init/SKILL.md @@ -0,0 +1,140 @@ +--- +name: init +description: Set up this cloned theme-elementary into a named theme, or manage its identity and capabilities later. During the pilot it can also run the local bootstrap (sibling clone of the tooling engine, local package ref, composer + npm install). 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 theme, or **manage** an already-set-up project. Always interactive: gather inputs first, confirm the resolved plan, then act. + +## Use for +- First run: optional pilot bootstrap (deps), rename starter tokens, pick which example sets 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*`/demo placeholders (step 8). +- Later: rename / re-prefix, toggle features (Tailwind, HMR). + +## 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 theme name. Show derived values back. 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 +Keep the developer oriented at every moment; great DX is the goal even through an AI skill. + +1. **State the plan up front** in one short message: the mode (setup / manage) and the ordered steps you will run for THIS request. For a setup-with-brief, that is: detect mode -> (offer) bootstrap deps -> gather + confirm identity and capabilities -> run init (which also removes the example sets the brief does not need) -> hand each feature to the scaffold skill. +2. **Maintain a live TODO list** on the host's task surface (`TodoWrite` in Claude Code): one entry per planned step, exactly one `in_progress` at a time, marked `completed` the moment it is done. Add entries as the work reveals them (a missing dep, each feature to scaffold). +3. **Announce each step with a short title** as you start it (e.g. "Running init", "Removing the shortcode example", "Handing the Testimonial block to scaffold") and report its outcome in one line. Never go silent during a long step. +4. **Confirm before anything destructive or installing** (the init rewrite, dependency installs), showing the exact resolved values. + +## 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. Setup: offer the pilot bootstrap +The rtCamp tooling packages are private/unpublished during the pilot. If `npm install` can resolve `@rtcamp/wp-tooling` from the registry and `composer install` resolves `rtcamp/wp-framework` from its VCS repo, no bootstrap is needed - skip to step 3. Otherwise ask: "Bootstrap local dependencies now? (clones the tooling engine, points npm at it, installs). y/n". On **yes**, with consent, run in order (tell the developer each step in <=30 words): + +```bash +nvm use # Node from .nvmrc + +# Sibling clone for the npm engine (skip if it already exists): +git clone git@github.com:rtCamp/wp-tooling.git ../wp-tooling +( cd ../wp-tooling && git checkout release/v1.0.0 ) # init/scaffold engine landed here + +# Local-only npm ref: this theme's only @rtcamp/* dependency is @rtcamp/wp-tooling. +npm pkg set devDependencies.@rtcamp/wp-tooling=file:../wp-tooling/node-packages/wp-tooling + +# wp-framework already resolves from the VCS "repositories" entry in composer.json (dev-main); +# composer update pulls it. For local framework development, optionally add a path repo instead: +# { "type": "path", "url": "../wp-framework", "options": { "symlink": false } } +composer update rtcamp/wp-framework +# --install-links is REQUIRED: it copies the file: @rtcamp package into node_modules instead of +# symlinking, so its peer deps resolve from this theme. Add --legacy-peer-deps on ERESOLVE. +npm install --install-links +``` +The `file:` edit is local-only. Note: init ALSO writes identity changes (name, namespace, pot path) into `package.json`/`composer.json`, so a blanket `git checkout package.json composer.json` would discard the rename. Before committing, revert ONLY the dependency-source line (the `@rtcamp/wp-tooling` `file:` ref, plus any path `repositories` entry and `composer.lock` you added) - keep every identity change. On **no**, skip to step 3 and surface installs as developer actions instead. + +### 3. Preconditions (verify; do not silently fix) +- `node_modules/@rtcamp/wp-tooling` exists (the engine needs it). If missing and bootstrap was declined, surface `npm install` as a developer action. +- Clean working tree (`git status`). Init rewrites files irreversibly; a clean tree is the only undo. If dirty, ask to commit/stash. +- Confirm this is a fresh clone meant to become a new theme, not the skeleton repo itself. + +### 4. Gather inputs +**Setup:** theme name (required, e.g. `Acme Blog` → namespace `rtCamp\Theme\Acme_Blog`, package `rtcamp/acme-blog`, text domain, constant/function/CSS prefixes, the `style.css` + `functions.php` headers; show these back); version (default `1.0.0`); which example sets to remove and which features to enable (defaults: keep all sets, hmr on, tailwind off). The engine derives the tokens itself; do not read the engine source to work them out - the mapping above is the contract, and the graph answers any deeper question (see the graphify policy in `AGENTS.md`). +**Manage:** which of identity / features to change. + +### 5. Confirm +Show the exact resolved values and the exact command. Get explicit consent; init is destructive. + +### 6. Run init (with consent) +`npm run init` also runs `npm run sync-ai`; that is expected. +```bash +# Setup: +npm run init -- --name="Acme Blog" --version=1.0.0 --yes \ + --remove-examples=shortcode,patterns --features=hmr,tailwind +# Manage: +npm run init -- --list +npm run init -- --enable=tailwind --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). + +**Setup WITH a feature brief - remove the example sets in THIS step (do not make it a separate round-trip):** remove every example set (`--remove-examples` with all keys, or the no-value form). Unlike a plugin's per-kind `AbstractModule` with a `get_classes()` array to keep-and-empty, this theme lists every class directly in `Main::CLASSES`, so there is nothing to keep-and-empty: the scaffold skill builds each briefed feature from scratch and wires its new `::class` line into `Main::CLASSES` itself (step 8). Example: brief = a custom Testimonial block + a footer credits shortcode -> `--remove-examples=block-extension,settings,shortcode,components,patterns`, then let scaffold build the two real features. + +### 7. After init +- Tailwind enabled → it added `src/css/frontend/tailwind.css` + `postcss.config.js` and pinned `@rtcamp/tailwind-config`; developer runs `npm install` (re-apply the `file:` ref first during the pilot). +- `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 phpcs` on the change and fix it. When a brief follows (step 8), leave the phpcs/PHPStan/test gates to the scaffold skill - init does not run them. +- **Refresh the LOCAL graph** with `graphify update .` (theme slice, tree-sitter, no API, seconds). The committed `graphify-out/graph.json` is a maintained baseline; your local refresh keeps queries accurate after a rename. If graphify is not installed, say so in one line; do not block. +- Report: new identity, example sets removed/kept, features toggled, outstanding developer actions. + +### 8. Implement the brief by handing off to scaffold (setup only, when features were described) +By this point step 6 has removed the example sets. 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 phpcs/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 dynamic `testimonial` block; a `[footer_credits]` shortcode). The scaffold skill owns conventions, the `wp-tooling add` call, wiring the new `::class` into `Main::CLASSES`, 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 theme 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\Theme\Acme_Blog`), **base_path** for module scaffolds (`inc/Modules/`), **tests_namespace** (e.g. `rtCamp\Theme\Acme_Blog\Tests`), **tests_path** (`tests/php`), **text_domain** and **slug** (e.g. `acme-blog`), the **theme display name**, and that **classes wire directly into `Main::CLASSES`** (no per-kind module file). With these provided, the scaffold skill skips its own `composer.json` / `style.css` / `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). + +End state: init has renamed and trimmed the example sets (step 6); the scaffold skill has implemented the real features. + +## Capability model +ONE "Select the example sets to include" prompt. Each is keep-or-remove; removing deletes it entirely (concrete classes, its `Main::CLASSES` line + `use` import, coupled regions, demo files). The sets and features match `bin/scaffold.config.js`: + +| Category | Keys | +|---|---| +| Editor & Frontend | `block-extension`, `shortcode`, `components`, `patterns`, `tailwind` (feature) | +| Admin | `settings` | +| Dev | `hmr` (feature) | + +- `block-extension` - Media-text block render-filter extension. +- `shortcode` - Author-bio shortcode. +- `components` - Example components (button, card). +- `patterns` - Page-creation block pattern. +- `settings` - Theme options settings page. + +Sets are kept by default; pass keys to `--remove-examples` to drop. Features: `hmr` on, `tailwind` off; toggle via `--features`/`--enable`/`--disable`. + +### Changing capabilities after setup +Add later → scaffold skill / `npx wp-tooling add /` (writes the class, wires it into `Main::CLASSES`). Remove later → delete its class file(s) under `inc/Modules//` plus its `Main::CLASSES` line and `use` import by hand. Do not re-run init to change the 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. `git clone`/`checkout` for setup are fine. Never do a destructive operation outside this theme directory; cloning a NEW sibling is additive and OK, deleting/overwriting existing out-of-repo files is not. +- Package managers only with consent: `npm run init`, and the pilot bootstrap of step 2 (sibling clone + local ref + `composer update`/`npm install`). 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 leave local-only `file:`/`path` edits uncommunicated: tell the developer to revert ONLY the dependency-source lines before commit (the `@rtcamp/wp-tooling` `file:` ref, any path `repositories` entry, `composer.lock`), keeping all identity changes init wrote into those files. +- 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`. +- Conventions renamed into: `AGENTS.md`, `.github/instructions/structure.instructions.md`. +- Local setup / features: `README.md`, `DEVELOPMENT.md`, `docs/hmr.md`, `docs/tailwind.md`, `docs/asset-building-process.md`. +- Graphify policy: `AGENTS.md`. diff --git a/.claude/skills/scaffold/SKILL.md b/.claude/skills/scaffold/SKILL.md new file mode 100644 index 00000000..35e2be71 --- /dev/null +++ b/.claude/skills/scaffold/SKILL.md @@ -0,0 +1,263 @@ +--- +name: scaffold +description: Add a scaffold (PHP class, dynamic block, shortcode, settings page, CI workflow) to this theme 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 theme's structure (`inc/Modules/`, namespace `\Modules\`, tests in `tests/php/`) and the private-package pilot. **Wiring difference from a plugin:** this theme lists every class directly in `inc/Main.php` `Main::CLASSES` (no per-kind `AbstractModule` with a `get_classes()` array), so a new artifact's `::class` line is inserted into `Main::CLASSES` (and a `use` import added), not into a module file. + +## Use for + +Dynamic block, shortcode, settings/admin page, plain Registrable service, framework module, CI/CD workflow. The engine also covers CPT, taxonomy, REST controller, cron, CLI, and user role - in a theme those belong in a **companion plugin** (see §3), so use them only for a deliberate exception and flag the placement. + +## Do not use for + +Refactors, bug fixes, hand-written features, anything no scaffold covers. Theme presentation that belongs in `theme.json`, a block pattern, a template part, or a block style (those are not class scaffolds). + +## 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 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` / `style.css` / `Main.php` reads below that only recover them. You still read a sample implementation for the registration pattern (next paragraph); sample it from a remaining example under `inc/Modules/` or from the framework abstract for the kind (`vendor/rtcamp/wp-framework/inc/Contracts/Abstracts/Abstract.php`); 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 `Main::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 - 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 `rtCamp\Theme\Elementary\` -> `inc/`). `autoload-dev.psr-4` for the tests namespace (`rtCamp\Theme\Elementary\Tests\` -> `tests/php/`). +- `package.json` -> scripts. For block scaffolds, parse `build:blocks` for `--output-path=` and pass as `--build_dir`. Default `assets/build/blocks`. +- Theme entry (`functions.php`) -> bootstrap class (`Main`) + `Main::CLASSES` (the list of every loaded class - this is where new classes wire in). +- 2-3 existing implementations of the same or a similar kind under `inc/Modules//` (e.g. `AuthorBio`, `ThemeOptions`, `MediaTextInteractive`): registration pattern, class-name suffix, sub-namespace, and how each is listed in `Main::CLASSES`. **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 theme's `composer.json` `autoload.psr-4` root (`rtCamp\Theme\Elementary` -> `inc/`); tests autoload via `\Tests` -> `tests/php/`. **The engine's own defaults target a different layout (`includes/...`, `Inc\...`, `tests/...`), so you MUST pass this theme's conventions on every `add` (§5).** Every kind wires into `inc/Main.php` `Main::CLASSES` (§6a) - there is no per-kind module file to edit. + +**Theme-appropriate kinds (lead with these):** + +| Scaffold | Source dir | Source ns | Test dir / ns | Wire into | +|---|---|---|---|---| +| `wp/block-dynamic` | `inc/Modules/Blocks/` + `src/blocks//` + `assets/build/blocks//` | `\Modules\Blocks` | `tests/php/` / `\Tests` | `inc/Main.php` (`Main::CLASSES`) | +| `wp/shortcode` | `inc/Modules/Shortcodes/` | `\Modules\Shortcodes` | `tests/php/` / `\Tests` | `inc/Main.php` (`Main::CLASSES`) | +| `wp/settings-page` | `inc/Modules/Settings/` | `\Modules\Settings` | `tests/php/` / `\Tests` | `inc/Main.php` (`Main::CLASSES`) | +| `wp/admin-page` | `inc/Modules/Admin/` | `\Modules\Admin` | `tests/php/` / `\Tests` | `inc/Main.php` (`Main::CLASSES`) | +| `wp/registrable` | `inc/Modules//` | `\Modules\` | `tests/php/` / `\Tests` | `inc/Main.php` (`Main::CLASSES`) | + +**Belongs in a companion plugin, not a theme** (`wp/cpt`, `wp/taxonomy`, `wp/rest`, `wp/cli`, `wp/cron`, `wp/user-role`): these register content types, endpoints, commands, or roles that WordPress guidelines place in a plugin - a theme switch must not drop a site's content or REST API. The engine still scaffolds them with the same conventions (`inc/Modules//`, ns `\Modules\`, tests `tests/php/`, wire into `Main::CLASSES`), so use them only for a companion plugin or a deliberate, stated exception - and flag the placement to the developer before scaffolding. + +Match the existing directory case exactly (`REST`, `CLI`, not `Rest`/`Cli`). + +**Modules host one kind each. No `Modules//...`.** A multi-kind feature spans the per-kind directories; each artifact wires its own `::class` into `Main::CLASSES`. + +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/block-dynamic`: block name, `register_hooks` action, `render()` markup with a `WP_Query` fixture, empty state, count cap, attribute filters. + - `wp/shortcode`: shortcode registered, output for valid attrs, escaping, default attrs, override by child theme (if it renders via `Util::get_template()`). + - `wp/settings-page` / `wp/admin-page`: page registered under the right menu, capability gate, `get_fields()` (settings) as the single source of truth, REST exposure. + - `wp/registrable`: hooks registered via `register_hooks()`, shared retrieval if `Shareable`. + - (companion-plugin kinds) `wp/cpt`: `post_type_exists()`, supports, REST exposure, attached taxonomies. `wp/rest`: route in `rest_get_server()->get_routes()`, permission check, schema. `wp/cron`: `wp_next_scheduled()`, callback fires, unschedule. `wp/cli`: `WP_CLI::add_command` registered, `__invoke`, dry-run. + +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 theme's conventions (§3)** - the engine defaults target a different layout. With `` = `rtCamp\Theme\Elementary` (or the renamed root) and `` = the module dir: + +```bash +npx wp-tooling add wp/ --non-interactive --json \ + --namespace='rtCamp\Theme\Elementary\Modules\' --base_path=inc/Modules/ \ + --tests_namespace='rtCamp\Theme\Elementary\Tests' --tests_path=tests/php --text_domain=elementary-theme \ + --slug= --class= --= ... +``` + +Per-scaffold inputs are not listed by `--help`; run once with `--dry-run` to discover required inputs (a block needs `slug`/`title`; a shortcode needs `tag`/`class`; a settings page needs `slug`/`title`). Dry-run preview: append `--dry-run`. Never use the interactive wizard. + +**Multi-kind / dependency order:** + +1. Artifacts in dependency order (e.g. a block or shortcode that queries a CPT comes after the CPT). +2. Re-read `ai.wiring` after each call. + +(There is no `wp/module` step: this theme has no per-kind `AbstractModule`. Each artifact wires into `Main::CLASSES` directly.) + +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`.** Pilot notes: npm installs need `npm install --install-links`; the framework (`rtcamp/wp-framework`) already ships, so ignore a `composer require rtcamp/wp-framework` 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 computes `targetFile` as a per-kind module file (e.g. `inc/Modules/.php`), which this theme does NOT use. The real wiring target is **`inc/Main.php`** - add the new class to `Main::CLASSES` and add its `use` import. +1. **Snippet** - the canonical snippet adds the class to a module's `get_classes()`; translate it to this theme: a new `::class,` line in the `Main::CLASSES` array plus the matching `use rtCamp\Theme\Elementary\Modules\\;` import, mirroring the sampled entries (e.g. `AuthorBio`, `ThemeOptions`). Show both. If patterns conflict, ask. +2. **Location** - `Main::CLASSES` carries no `// scaffold::classes` anchor, so insert the `::class` after the last existing entry in the array (and add the `use` import in the import group, alphabetical with the others). Anchor present -> after it; else skip and print as a manual instruction. +3. **Consent** - show targetFile (`inc/Main.php`) + line range + description + rendered snippet (both the `Main::CLASSES` line and the `use` import). 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 phpcs:fix` (phpcbf), `composer phpcs` (phpcs), `composer phpstan` (PHPStan), `npm run lint:js`. **Run the PHP linters inside wp-env** - the host PHP may be newer than the pinned WPCS supports and will abort the sniffs (see Pilot environment). Never silence a real phpcs/phpstan finding with a blanket ignore; fix it, or escalate (§8). | + +Frameworks per kind: + +| Kind | Framework | +|---|---| +| `wp/shortcode`, `wp/settings-page`, `wp/admin-page`, `wp/registrable` (and companion-plugin `wp/cpt`/`wp/taxonomy`/`wp/cron`/`wp/cli`/`wp/rest`) | 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; AGENTS.md graphify policy). + +Then report: + +- Files written, grouped by top-level directory. +- Wiring applied: `inc/Main.php` line, the `Main::CLASSES` entry + `use` import added. +- Tests authored and pass count per file. +- Lint + PHPStan result. +- Outstanding developer actions: composer / npm installs (pilot: `--install-links`), `npm run build` (blocks), secrets to set, branch-protection note (CI). + +## Pilot environment + engine quirks (this theme) + +**Test env + linters (`wp-env`):** +- Always use `npx wp-env` (or `node_modules/.bin/wp-env`), never bare `wp-env`. +- **Run PHP linters inside wp-env (PHP 8.2).** The host PHP may be newer than the pinned `wp-coding-standards/wpcs` supports, which makes the sniffs throw deprecation errors and abort. Run e.g. `npx wp-env run cli --env-cwd=/var/www/html/wp-content/themes/$(basename "$PWD") -- vendor/bin/phpcs ` (PHPStan tolerates newer PHP, so `composer phpstan` is fine on the host). +- 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`). The theme's default ports are 5890 (dev) / 5891 (tests). +- `wp-env start` can flake on a transient image pull (TLS timeout); one retry is allowed, and exit 0 does not mean "up" - confirm the start output reports success. +- `pretest:php` runs `composer install` in the `cli` container; if `npm run test:php` fails on it, run PHPUnit directly: `npx wp-env run cli --env-cwd=/var/www/html/wp-content/themes/$(basename "$PWD") -- vendor/bin/phpunit -c phpunit.xml.dist`. + +**Generated-code quirks (write the code right up front; these survive `composer phpcs:fix`):** +- **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 phpcs:fix` force-qualifies `WP_Error` (Slevomat `FullyQualifiedExceptions` treats `*Error` as an exception) and then strips the now-unused imports, including docblock-only ones; PHPStan (scanning `inc/`) then reports `class.notFound`. Writing them fully-qualified avoids the fix -> phpstan round-trip. +- **(companion-plugin) `wp/rest` overridden controller methods must match WP core's untyped signatures.** Adding a parameter type to a `WP_REST_Controller` override is a fatal LSP/contravariance violation that crashes `wp-env start`. Rewrite every overridden method with untyped params/return and express the types in PHPDoc only. +- **(companion-plugin) `wp/rest` route is double-versioned.** The generated `$namespace` already includes the version yet `register_routes()` appends `/v{version}` again. Pick one source and 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 phpcs:fix` (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 ``, `vendor/bin/phpstan analyse `) and scope `composer phpcs:fix` to them; a repo-wide non-zero exit is not necessarily your code. The theme ships gate-clean (in wp-env), 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 theme directory. +- 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 phpcs:fix` -> `composer phpcs` -> `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 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 00000000..1f26be59 --- /dev/null +++ b/.claude/skills/scaffold/evals/evals.json @@ -0,0 +1,50 @@ +{ + "skill_name": "scaffold", + "evals": [ + { + "id": 1, + "prompt": "Add a dynamic Gutenberg block called pricing-table to my theme. The theme uses rtCamp\\Theme\\Elementary\\ -> inc/ PSR-4 autoload and wires classes directly into inc/Main.php Main::CLASSES (e.g. MediaTextInteractive::class). Blocks live at src/blocks/ and existing block.json names use the vendor prefix 'elementary' (elementary/hero, elementary/cta). Scaffold the pricing-table block following these conventions.", + "expected_output": "src/blocks/pricing-table/ created with block.json (name: elementary/pricing-table), edit.js, index.js, render.php, plus a Blocks PHP class under inc/Modules/Blocks/ (namespace rtCamp\\Theme\\Elementary\\Modules\\Blocks) and a PHPUnit test stub. The block class is wired into inc/Main.php Main::CLASSES with a matching use import (NOT into a per-kind module get_classes()). The vendor prefix 'elementary' is detected from an existing block.json. No npm install or wp-scripts build executed.", + "files": [], + "expectations": [ + "The skill picks the wp/block-dynamic scaffold (not a guess across multiple candidates).", + "The skill introspects the theme before invoking the engine and reports: namespace rtCamp\\Theme\\Elementary, base path inc, the src/blocks/ source dir, and that classes wire directly into Main::CLASSES (no AbstractModule / get_classes).", + "The skill introspects an existing src/blocks/*/block.json to confirm the 'elementary' vendor prefix and the src/blocks/ source directory.", + "The engine invocation passes this theme's conventions (--namespace=rtCamp\\Theme\\Elementary\\Modules\\Blocks, --base_path=inc/Modules/Blocks, --tests_namespace=rtCamp\\Theme\\Elementary\\Tests, --tests_path=tests/php), not the engine defaults.", + "The generated block.json name is 'elementary/pricing-table' (not 'wp-tooling/pricing-table' or unprefixed 'pricing-table').", + "The engine writes block.json, edit.js, index.js, render.php, the Blocks PHP class, and a PHPUnit test stub.", + "The wiring inserts the block class ::class line into inc/Main.php Main::CLASSES plus its use import, NOT into a per-kind module file or get_classes() array.", + "Wiring is shown with an explicit consent prompt before applying.", + "The skill does NOT run npm install or wp-scripts build." + ] + }, + { + "id": 2, + "prompt": "I need a shortcode in my theme that renders a formatted reading-time estimate for the current post. The theme uses rtCamp\\Theme\\Elementary\\ -> inc/ PSR-4. Existing shortcodes live in inc/Modules/Shortcodes/ (e.g. AuthorBio) and are wired into inc/Main.php Main::CLASSES. Scaffold the reading_time shortcode following these conventions.", + "expected_output": "A new PHP class under inc/Modules/Shortcodes/ (namespace rtCamp\\Theme\\Elementary\\Modules\\Shortcodes) registering the [reading_time] shortcode, plus a PHPUnit test stub under tests/php/. The class is wired into inc/Main.php Main::CLASSES with a matching use import. No composer require executed.", + "files": [], + "expectations": [ + "The skill picks the wp/shortcode scaffold (not a guess across multiple candidates).", + "The skill introspects the theme and samples an existing shortcode (e.g. AuthorBio) for the registration pattern, class-name style, and sub-namespace.", + "The engine invocation passes the theme conventions (--namespace=rtCamp\\Theme\\Elementary\\Modules\\Shortcodes, --base_path=inc/Modules/Shortcodes, --tests_namespace=rtCamp\\Theme\\Elementary\\Tests, --tests_path=tests/php, --text_domain=elementary-theme).", + "The engine writes the shortcode PHP class and a PHPUnit test stub.", + "The wiring inserts the class ::class line into inc/Main.php Main::CLASSES (with its use import), NOT into a per-kind module file or a get_classes() array.", + "Wiring is shown with an explicit consent prompt before applying.", + "The skill expands the test stub into a real suite and drives implementation TDD-first, leaving no markTestIncomplete.", + "The skill does NOT run composer require, composer install, or npm install on the user's behalf." + ] + }, + { + "id": 3, + "prompt": "Add a testimonial custom post type to my theme so editors can manage testimonials.", + "expected_output": "The skill recognises that a custom post type belongs in a companion plugin, not a theme (WordPress guidelines: switching themes must not drop a site's content), flags the placement to the developer, and asks for a decision before scaffolding. It does NOT silently scaffold a CPT into the theme. If the developer confirms a deliberate exception, it scaffolds with the theme conventions (inc/Modules/PostTypes/, namespace rtCamp\\Theme\\Elementary\\Modules\\PostTypes, wiring into Main::CLASSES).", + "files": [], + "expectations": [ + "The skill identifies wp/cpt as the relevant scaffold but flags that a CPT belongs in a companion plugin, not a theme, per WordPress guidelines.", + "The skill stops and asks the developer before scaffolding the CPT into the theme (does not silently proceed).", + "If the developer confirms a deliberate exception, the engine invocation uses the theme conventions (--namespace=rtCamp\\Theme\\Elementary\\Modules\\PostTypes, --base_path=inc/Modules/PostTypes, --tests_namespace=rtCamp\\Theme\\Elementary\\Tests, --tests_path=tests/php), wiring the class into Main::CLASSES.", + "The skill does NOT run composer require, composer install, or npm install on the user's behalf." + ] + } + ] +} diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md new file mode 100644 index 00000000..1cf4b4a3 --- /dev/null +++ b/.claude/skills/setup/SKILL.md @@ -0,0 +1,363 @@ +--- +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 (blocks, shortcodes, settings pages, 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/skills/setup/SKILL.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 (block, shortcode, settings page, 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 vs plain WordPress. +- **Languages needed**: PHP, JS (blocks, scripts), CSS/SCSS. +- **Features wanted**: block, shortcode, settings page, 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. rtCamp\\Theme\\AcmeBlog) +4. What directory holds the PHP source? (e.g. inc/ 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 block - what should it render? (I need a slug/title.) +``` + +### 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.mjs .stylelintrc.json \ + 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.mjs` exists | +| CSS or SCSS present | `lint/stylelint` | `.stylelintrc.json` 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 (theme-appropriate first): + +| Developer said | Scaffold ID | +|---|---| +| Gutenberg block | `wp/block-dynamic` | +| Shortcode | `wp/shortcode` | +| Settings / options page | `wp/settings-page` | +| Admin page | `wp/admin-page` | +| Plain service | `wp/registrable` | +| CI pipeline | `ci/cd-wporg` (or other CI scaffold) | +| (companion plugin) CPT / taxonomy / REST / cron / CLI | `wp/cpt` / `wp/taxonomy` / `wp/rest` / `wp/cron` / `wp/cli` | + +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 a theme, flag CPT/taxonomy/REST/cron/CLI requests as companion-plugin work (WordPress guidelines keep content and endpoints out of themes) before scaffolding them. + +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: rtCamp\Theme\AcmeBlog, path: inc/) + 3. lint/phpcs/full → phpcs.xml.dist + 4. lint/phpstan → phpstan.neon.dist + 5. lint/eslint → eslint.config.mjs + 6. setup/phpunit → phpunit.xml.dist, tests/bootstrap.php + +Phase B - Feature scaffolds: + 7. wp/shortcode → inc/Modules/Shortcodes/FooterCredits.php + (namespace: rtCamp\Theme\AcmeBlog\Modules\Shortcodes, + wires its ::class into Main::CLASSES) + +Skipped (already present): none. + +Not in catalogue (manual tasks): none. + +Confirm? Or adjust (e.g. "remove phpunit", "use vip phpcs", "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='rtCamp\Theme\AcmeBlog' --base-path='inc' +``` + +For lint and test setup scaffolds: + +```bash +npx wp-tooling add lint/phpcs/full --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=inc +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 `scaffold/SKILL.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 (into `Main::CLASSES`), complete the TDD loop (see `scaffold/SKILL.md` §7), then move to the next. + +For `wp/shortcode`, pass the same project conventions detected in Stage 1 (namespace, base path) - the engine defaults assume a different layout and will be wrong for any other project: + +```bash +npx wp-tooling add wp/shortcode \ + --non-interactive --json --cwd . \ + --namespace='rtCamp\Theme\AcmeBlog\Modules\Shortcodes' --base_path='inc/Modules/Shortcodes' \ + --tests_namespace='rtCamp\Theme\AcmeBlog\Tests' --tests_path=tests/php \ + --tag=footer_credits --class=FooterCredits +``` + +The engine emits `ai.wiring` (where to register the class) and a thin test stub. Show the wiring snippet, get consent, apply it into `Main::CLASSES` (plus the `use` import). Then turn the stub into a real test suite (happy path, attrs, escaping, edge cases) and implement 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 (`scaffold/SKILL.md` §7 G): `composer phpcs:fix` (phpcbf) -> `composer phpcs` (the project's `phpcs.xml.dist` ruleset) -> `composer phpstan` (the project's `phpstan.neon.dist`), resolving every finding in the generated code. **Run the PHP linters inside wp-env** when the host PHP is newer than the pinned WPCS. 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 shortcode, but Phase A skipped setup/phpunit. +The wp/shortcode 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 `rtCamp\Theme\AcmeBlog` maps to `inc/`. +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. `"rtCamp\\Theme\\AcmeBlog\\"`) - **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": "rtcamp/acme-blog", + "type": "wordpress-theme", + "require": { "php": ">=8.2" }, + "autoload": { + "psr-4": { "rtCamp\\Theme\\AcmeBlog\\": "inc/" } + } + } + +Create it? [yes / skip PSR-4 / give me the values to use] +``` + +### Wiring: feature scaffolds + +Same as `scaffold/SKILL.md` §6a - `ai.wiring`. The wiring target is `inc/Main.php` `Main::CLASSES` (this theme lists classes directly there, not in a per-kind module). Show diff, get consent, apply. + +### 6. Consolidated final report + +``` +Setup complete. + +Files written (Phase A): + .editorconfig + phpcs.xml.dist + phpstan.neon.dist + eslint.config.mjs + phpunit.xml.dist + tests/bootstrap.php + composer.json (PSR-4 autoload added, rtCamp\Theme\AcmeBlog → inc/) + +Files written (Phase B): + inc/Modules/Shortcodes/FooterCredits.php + tests/php/inc/Modules/Shortcodes/FooterCreditsTest.php + +Wiring applied: + inc/Main.php - added FooterCredits::class to Main::CLASSES (+ use import). + +Tests: + tests/php/inc/Modules/Shortcodes/FooterCreditsTest.php - passing. + +Developer actions (run these yourself): + + composer dump-autoload --optimize + + (any composer require / npm install lines the scaffolds reported) + +Scripts to add (shown, not applied): the project's lint/test scripts. + +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) | `vendor/rtcamp/wp-framework/phpcs.xml.dist`, WordPress-Core + Extra + Docs + VIP-Go | +| `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 | 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. +- Block slug and vendor prefix. +- Shortcode tag and class name. +- Settings/admin page slug and title. +- 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 the project 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 (`graphify update .`) 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 phpcs:fix` -> `composer phpcs` -> `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`. +- One-off scaffold additions are handled by the companion `scaffold` skill (`scaffold/SKILL.md`). diff --git a/.claude/skills/setup/evals/evals.json b/.claude/skills/setup/evals/evals.json new file mode 100644 index 00000000..30708c13 --- /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 theme called My Theme in the rtCamp\\Theme\\MyTheme namespace under inc/. I want PHPCS, PHPStan, and PHPUnit. Then add a reading-time shortcode that I'll fill in later.", + "expected_output": "Two-phase plan executed across the greenfield project: Phase A installs editorconfig + psr4 + phpcs/full + phpstan + phpunit (skipping eslint/stylelint/jest/pa11y with reasons), Phase B scaffolds the wp/shortcode. A minimal composer.json (type wordpress-theme) 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 executed.", + "files": [], + "expectations": [ + "Stage 0: The skill parses the request and identifies all stated facts (theme, 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/full + lint/phpstan + setup/phpunit for Phase A, and wp/shortcode 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 (type wordpress-theme) with the JSON-escaped namespace key 'rtCamp\\\\Theme\\\\MyTheme\\\\'.", + "Stage 5: Defaults to adding setup/phpunit before running wp/shortcode 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, scripts to add to manifests, skipped-with-reasons, outstanding manual tasks; and the shortcode class wired into Main::CLASSES.", + "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 theme. It's at the repo root. composer.json already declares rtCamp\\Theme\\Acme\\ -> inc/ PSR-4 autoload and there's already a phpcs.xml.dist with the WordPress standard. 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 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 (does not re-add PHPCS).", + "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 (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/.github/instructions/framework-php.instructions.md b/.github/instructions/framework-php.instructions.md new file mode 100644 index 00000000..fe737ee6 --- /dev/null +++ b/.github/instructions/framework-php.instructions.md @@ -0,0 +1,56 @@ +--- +applyTo: "**/*.php" +description: "Framework + WordPress rules for PHP. Canonical source, shipped by rtcamp/wp-framework." +--- + +# PHP rules: framework, WordPress, security, tests + +`rtcamp/wp-framework` (`rtCamp\WPFramework`) 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` and `Abstract{PostType,Taxonomy,Block,Shortcode,RESTController,SettingsPage,AdminPage,UserRole}`. + +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 `