Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion node-packages/wp-tooling/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ src/
bin/wp-tooling.js thin shim requiring ../src/cli/index.js
scaffolds/ top-level categories: ci/, lint/, setup/, wp/
docs/ ai-orchestration.md, authoring-scaffolds.md, editor-setup.md,
examples.md, wp-framework-contract.md
examples.md, wp-primitives-contract.md
skills/ README.md + scaffold/ + setup/ + devtools-setup/ (each with SKILL.md + evals/);
*-workspace/ dirs are gitignored eval scratch, not shipped content
tests/ mirrors src/'s 8 dirs, plus fixtures/ and a skills/ test dir (10 total)
Expand Down
47 changes: 45 additions & 2 deletions node-packages/wp-tooling/CHANGELOG.md

Large diffs are not rendered by default.

20 changes: 17 additions & 3 deletions node-packages/wp-tooling/docs/ai-orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The engine commits to the following on a successful run. Skills can rely on thes
- `scaffold.kind` is `"package"` for `source: "package"` scaffolds (no files written, only deps and wiring; the bundled examples are `utility/*`) and `"template"` otherwise. Remote (sources) scaffolds also report `kind: "template"` — they render Mustache the same way as local ones; where the scaffold lives is an implementation detail the orchestrator does not need to branch on. Callers branch on `kind` rather than checking `engine.wrote.length === 0`.
- `wp-tooling list --json` entries carry an `origin` of `"default"`, `"project"`, or `"remote"`. Remote scaffolds come from a repo's cached index (`sources.json` → each repo's `index.json`), so their `counts` is `null` (unknown until `add`); local scaffolds carry real `counts`. `list` is online-preferred with a cache fallback: it reads the index (cached, ETag-validated), and a `warnings` array reports any source that was unreachable and uncached. The top-level `{ scaffolds, warnings }` shape carries those notes.
- The engine core has zero dependency on the TTY UI kit. AI orchestration mode never loads any terminal-UI primitive. Skills can rely on the engine being usable from any context, including non-TTY containers, CI runners, and headless test harnesses.
- File-based `discover_from` (`composer.json:<dot.path>`, `package.json:<dot.path>`, `config:<key>` from `.wp-tooling.json`) is resolved by the engine itself, before manifest defaults are applied, with precedence **`supplied → discovered → default`**. A value the skill passes explicitly always wins. A missing or malformed source file is ignored and the input falls back to its `default` — the engine never throws because a project file is absent or unparsable. `code:*` and `plugin-header:*` sources are **not** engine-resolved and remain the skill's responsibility (§6).
- File-based `discover_from` (`composer.json:<dot.path>` including `autoload.psr-4` and `autoload-dev.psr-4`, `package.json:<dot.path>`, `plugin-header:<header-name>` from the plugin or theme entry header, `config:<key>` from `.wp-tooling.json`) is resolved by the engine itself, before manifest defaults are applied, with precedence **`supplied → discovered → default`**. A value the skill passes explicitly always wins. A missing or malformed source file is ignored and the input falls back to its `default` — the engine never throws because a project file is absent or unparsable. `code:*` sources are **not** engine-resolved and remain the skill's responsibility (§6).
Comment thread
Copilot marked this conversation as resolved.
Resolved
- A scaffold may carry an optional `feature` block, marking it a **toggleable feature** (the bundled example is `setup/tailwind`). This block is **invisible to `add`/`execute`**: it never appears in the result, and `add` behaves identically whether or not it is present. Turning features on and off is a separate surface (§13).

## 4. Skill responsibilities
Expand Down Expand Up @@ -222,8 +222,8 @@ The skill discovers project shape from existing artifacts. No config file is req

The engine now resolves the **file-based** `discover_from` sources itself, so the division of labour is:

- **`composer.json:<dot.path>`, `package.json:<dot.path>`, `config:<key>` → the engine resolves these.** The skill does not need to pre-read these files and pass the value; the engine reads them from `--cwd` and fills the input before applying the `default`. The most important case: `namespace` and `tests_namespace` on every `wp/*` scaffold declare `discover_from: composer.json:autoload.psr-4`, so they are **auto-filled from the project's first PSR-4 root** whenever a `composer.json` exists — with the kind sub-namespace preserved: the discovered root replaces only the *first segment* of the manifest default, so a default of `Inc\Cli` and a PSR-4 root of `Acme\Blog` yield `Acme\Blog\Cli` (and `Inc\Tests\Cli` yields `Acme\Blog\Tests\Cli`). A skill that previously always passed `--namespace` no longer has to — though doing so is still safe (see precedence below).
- **`code:*` and `plugin-header:*` → still the skill's job.** The engine cannot read a bootstrap class, sample existing patterns, or parse a plugin header. For these the engine returns the manifest `default`; the skill must do the introspection in this section and pass the value in.
- **`composer.json:<dot.path>`, `package.json:<dot.path>`, `plugin-header:<header-name>`, `config:<key>` → the engine resolves these.** The skill does not need to pre-read these files and pass the value; the engine reads them from `--cwd` and fills the input before applying the `default`. The most important case: `namespace` and `tests_namespace` on every `wp/*` scaffold declare `discover_from: composer.json:autoload.psr-4`, so they are **auto-filled from the project's first PSR-4 root** whenever a `composer.json` exists, with the kind sub-namespace preserved: the discovered root replaces only the *first segment* of the manifest default, so a default of `Inc\Cli` and a PSR-4 root of `Acme\Blog` yield `Acme\Blog\Cli` (and `Inc\Tests\Cli` yields `Acme\Blog\Tests\Cli`). A skill that previously always passed `--namespace` no longer has to, though doing so is still safe (see precedence below).
- **`code:*` → still the skill's job.** The engine cannot read a bootstrap class or sample existing patterns. For these the engine returns the manifest `default`; the skill must do the introspection in this section and pass the value in.

Rules the skill can rely on:

Expand Down Expand Up @@ -265,6 +265,20 @@ Fall through these in order:
3. **No anchor and no clear pattern neighbourhood.** The bootstrap method exists (from §6) but there are no prior examples of this scaffold type. Insert at a sensible position in the bootstrap method (typically just before the closing brace), and explicitly tell the developer this is a best-effort placement.
4. **No bootstrap method either.** Skip wiring, surface the snippet as a manual instruction in the report, let the developer place it.

Levels 2 and 3 place the snippet relative to code that may not survive. rtCamp
skeletons wrap each optional capability in `// wp:example:<key>` … `//
wp:example:<key>:end` markers, and `wp-tooling init` deletes a marked region
**including its body** when the developer drops that capability
(`src/init/examples.js`). The last sampled occurrence of a pattern is very often
the last line inside such a region, so appending after it puts the newly wired
class inside a block that later disappears — silently, and long after the run
that placed it.

**Never place the snippet between a `// wp:example:<key>` opener and its
`:end`.** Insert after the last occurrence that sits outside every region
instead, and say which line you chose and why. This holds at level 1 too: an
anchor can itself sit inside a region.

At every level the skill **shows the developer the snippet, the target file, the chosen line range, and the reason for the choice** before applying. The developer can apply, redirect to a different location, edit the snippet, or skip.

### Anchor restoration (optional, never automatic)
Expand Down
21 changes: 15 additions & 6 deletions node-packages/wp-tooling/docs/authoring-scaffolds.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class {{class}} { }

```mustache
{{#with_logging}}use {{namespace}}\Services\Logger;
{{/with_logging}}use rtCamp\WPFramework\Contracts\Interfaces\Registrable;
{{/with_logging}}use rtCamp\WPPrimitives\Contracts\Interfaces\Registrable;
```

`{{#key}}...{{/key}}` renders the inner block when `vars[key]` is truthy. Truthy means a non-empty string that is not one of `"false"`, `"no"`, `"0"` (case-insensitive).
Expand Down Expand Up @@ -139,8 +139,14 @@ exactly as if `discover_from` were not set).

- `input:<other-key>` — derive from another resolved input (e.g. `class` from `name`, with a `pascal-case` transform).
- `composer.json:<dot.path>` / `package.json:<dot.path>` — a string value at a dotted path. The special selector `autoload.psr-4` (or `autoload.psr-0`) yields the **root namespace** for ordinary inputs (first map key, trailing `\` stripped) and the **root directory** for path inputs (that same entry's value; the first element if it is a list). Either way the discovered root replaces only the *first segment* of the input's `default`, keeping the scaffold's sub-namespace or sub-directory: with a map of `Acme\Blog\` → `inc/`, a `namespace` default of `Inc\Cli` yields `Acme\Blog\Cli` and a `base_path` default of `includes/Cli` yields `inc/Cli`. Both come from the same map entry, so a class is never namespaced into the autoload root while being written outside it. A path input whose `default` has no sub-directory resolves to the root directory itself, and a PSR-4 target of `./` leaves just the sub-directory.
- `composer.json:autoload-dev.psr-4` — the same graft against the **dev** autoload map, which is where a project declares its test tree. Use it for `tests_path`: a dev map of `Acme\Blog\Tests\` → `tests/php/` turns a default of `tests/Cli` into `tests/php/Cli`, so the generated test lands where the project's PHPUnit testsuite actually looks. Without it a test is written to the manifest default and is silently never collected.
- `plugin-header:<header-name>` — a value from the project's WordPress entry header, named lowercased with spaces hyphenated: `plugin-header:text-domain` reads `Text Domain:`. The engine looks for a root-level `*.php` carrying `Plugin Name:` and falls back to a theme's `style.css` carrying `Theme Name:`, so this resolves for plugins and themes alike. Only the header comment is scanned, so a `Foo: bar` in the code below is never mistaken for a header.
- `config:<dot.path>` — a string value from the project's `.wp-tooling.json` (e.g. `config:textDomain`).

`validate` rejects a `discover_from` whose prefix is not one of the above. An unrecognised
source used to resolve to nothing and fall through to the `default` in silence, which is how
seven scaffolds shipped for months declaring a text-domain lookup that never ran.

Example — auto-fill the namespace from the consuming project's composer.json, falling back to a sensible default:

```json
Expand All @@ -163,6 +169,9 @@ Available via `transform`:
- `snake-case`: `qm-export` → `qm_export`
- `upper-snake-case`: `wporg-username` → `WPORG_USERNAME`
- `json-escape`: `Acme\Blog` → `Acme\\Blog` (embed a PHP namespace in a JSON snippet)
- `php-escape`: escapes backslashes and apostrophes for a PHP string enclosed in single quotes.
- `js-string`: emits a complete JavaScript string literal, escaping control characters and choosing the quote style used by the formatter. Templates do not add surrounding quotes.
- `php-project-root`: derives a PHP expression from a project-relative directory (`inc/Blocks` → `dirname( __DIR__, 2 )`, `.` → `__DIR__`). Block and script module scaffolds use it to find their project root with custom source layouts.
- `shell-escape`: `my plugin` → `'my plugin'` (POSIX-quote one shell argument; safe values like `wp-content/plugins/x` stay bare)

Transforms are applied after the value is resolved. Add new transforms in `src/scaffolds/render.js` (`TRANSFORMS` map).
Expand Down Expand Up @@ -207,8 +216,8 @@ The engine never edits existing files. When a scaffold needs to register itself

Each entry:

- `target_file`: path to the file. Placeholders allowed.
- `anchor`: grep-able string that hints at insertion point (e.g. `// scaffold:cli-commands`). Anchors are useful but not load-bearing; the AI falls back to pattern sampling.
- `target_file`: path to the file, relative to the project root. Placeholders allowed. The engine normalises the rendered path, so `{{base_path}}/../Modules/Cli.php` reaches the AI as `includes/Modules/Cli.php` — write the `..` form rather than duplicating a path input. An entry whose path still resolves outside the project after normalising is dropped with a warning.
- `anchor`: grep-able string that hints at insertion point (e.g. `// scaffold:cli-commands`). Anchors are useful but not load-bearing; the AI falls back to pattern sampling. Either way it refuses to insert inside a `// wp:example:<key>` … `:end` region, since `wp-tooling init` deletes those body and all.
- `snippet_template`: the snippet to insert. Mustache placeholders are rendered before the snippet is emitted.
- `description`: explains intent. The AI uses this when asking the developer for consent.

Expand All @@ -223,7 +232,7 @@ declares `files: []`, the `composer_dependencies` entry that provides the class,
snippet that constructs it. `execute()` reports `scaffold.kind: "package"` and writes nothing.

- `module_class`: the fully qualified PHP class the scaffold wires up (e.g.
`rtCamp\\WPFramework\\Utils\\Cache` — doubled backslashes, since this is JSON). Manifest metadata
`rtCamp\\WPPrimitives\\Utils\\Cache` — doubled backslashes, since this is JSON). Manifest metadata
documenting which vendor class the `wiring[]` snippet constructs. Optional, but set it on every
package scaffold.

Expand Down Expand Up @@ -422,6 +431,6 @@ Copy the closest match, rename, adjust. Most scaffolds are 20-50 lines of JSON p

---

## When to involve `wp-framework`
## When to involve `wp-primitives`

If your new scaffold extends or uses a class from `rtcamp/wp-framework` that does not yet exist, add the contract to [docs/wp-framework-contract.md](wp-framework-contract.md) and open an issue on `wp-framework`. Do not ship a scaffold that references an unreleased class. It will break for consumers running `composer install`.
If your new scaffold extends or uses a class from `rtcamp/wp-primitives` that does not yet exist, add the contract to [docs/wp-primitives-contract.md](wp-primitives-contract.md) and open an issue on `rtCamp/wp-primitives`. Do not ship a scaffold that references an unreleased class. It will break for consumers running `composer install`.
18 changes: 12 additions & 6 deletions node-packages/wp-tooling/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Each example shows: what the developer typed, the AI's clarifying questions and
Phase A — Project setup:
1. setup/editorconfig → .editorconfig
2. setup/psr4 → composer.json wiring (Acme\InventoryManager → includes/)
3. lint/phpcs/full → phpcs.xml.dist (rtCamp full standard)
3. lint/phpcs/full → phpcs.xml.dist (rtCampWP) + composer.json wiring
4. lint/phpstan → phpstan.neon.dist (level 5)
5. lint/eslint → eslint.config.js
6. setup/phpunit → phpunit.xml.dist, tests/bootstrap.php
Expand Down Expand Up @@ -70,6 +70,12 @@ npx wp-tooling add wp/cli --non-interactive --json --cwd . --name=im

**Developer:** yes.

> For `lint/phpcs/full`, `rtcamp/wp-phpcs` is not on Packagist, so I would add its GitHub repository to `repositories` and allow `dealerdirect/phpcodesniffer-composer-installer` under `config.allow-plugins` in `composer.json`.
>
> Apply? [yes / skip]

**Developer:** yes.

(AI applies wirings for `Plugin.php` for the CPT, taxonomy, CLI registrations one at a time.)

**Final report:**
Expand All @@ -89,8 +95,8 @@ Developer actions:

composer dump-autoload --optimize

composer require rtcamp/wp-framework:^1.0
composer require --dev squizlabs/php_codesniffer:^3.7 wp-coding-standards/wpcs:^3.0 phpunit/phpunit:^12.0 yoast/phpunit-polyfills:^4.0 brain/monkey:^2.6
composer require rtcamp/wp-primitives:^2.0
composer require --dev rtcamp/wp-phpcs:^1.0 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:
Expand Down Expand Up @@ -353,7 +359,7 @@ Tests:
> 2. wp/taxonomy → includes/Taxonomies/Category.php
> 3. wp/rest → includes/Rest/ProductController.php
>
> All three pull `rtcamp/wp-framework:^1.0`. Each class extends the matching abstract from the framework and is wired through the consumer's module.
> All three pull `rtcamp/wp-primitives:^2.0`. Each class extends the matching abstract from the framework and is wired through the consumer's module.
>
> Confirm?

Expand Down Expand Up @@ -390,7 +396,7 @@ Files written: 6 (3 production + 3 tests).
Wiring applied: 3 entries in includes/Plugin.php.

Developer actions:
composer require rtcamp/wp-framework:^1.0
composer require rtcamp/wp-primitives:^2.0
composer dump-autoload --optimize
```

Expand Down Expand Up @@ -465,7 +471,7 @@ Files written (10), wiring applied (1 in Plugin.php for the CLI command).

Outstanding developer actions:
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
composer require --dev automattic/vipwpcs:^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
```

---
Expand Down
Loading
Loading