diff --git a/.agents/README.md b/.agents/README.md
new file mode 100644
index 0000000..84922a5
--- /dev/null
+++ b/.agents/README.md
@@ -0,0 +1,39 @@
+# .agents
+
+This folder contains portable agent-specific assets for {{PLUGIN_NAME}}.
+These assets are designed to be reusable across different AI systems and tools.
+
+---
+
+## Structure
+
+| Folder | Purpose |
+|---|---|
+| `skills/` | Portable, reusable agent skills — focused tasks agents can perform |
+| `agents/` | Agent persona definitions — describe specialist roles agents can adopt |
+
+---
+
+## Difference between `.github/` and `.agents/`
+
+| `.github/` | `.agents/` |
+|---|---|
+| GitHub-specific (Copilot, Actions, instructions) | Portable across AI systems |
+| Workflows, prompts, reports, tasks | Skills and personas |
+| Tied to GitHub platform features | Usable in any AI tool or agent framework |
+
+---
+
+## Usage
+
+- AI agents should read `AGENTS.md` first, then consult relevant skills and personas here.
+- Skills describe *how* to perform a specific task in this repo context.
+- Agents describe *who* the AI should act as for a given type of work.
+
+---
+
+## Adding skills and agents
+
+- Add new skills as folders under `.agents/skills/`, each with a `SKILL.md` file.
+- Add new agent personas as Markdown files under `.agents/agents/`.
+- Keep skills focused — one skill, one task type.
diff --git a/.agents/agents/README.md b/.agents/agents/README.md
new file mode 100644
index 0000000..b819dc8
--- /dev/null
+++ b/.agents/agents/README.md
@@ -0,0 +1,27 @@
+# .agents/agents
+
+This folder contains agent persona definitions for {{PLUGIN_NAME}}.
+
+---
+
+## What is an agent persona?
+
+An agent persona describes the role, expertise, and behaviour an AI agent should adopt for a specific type of work in this repository.
+Personas are not skills — they describe *who the agent is*, not *what it does*.
+
+---
+
+## Available agents
+
+| Agent | Description |
+|---|---|
+| `plugin-architect.md` | WordPress plugin architecture specialist |
+
+---
+
+## Adding new agents
+
+1. Create a file named `{role}.md` in this folder.
+2. Describe the agent's role, expertise, and working style.
+3. Reference specific skills from `.agents/skills/` where relevant.
+4. Add it to the table in this README.
diff --git a/.agents/agents/plugin-architect.md b/.agents/agents/plugin-architect.md
new file mode 100644
index 0000000..ae5262d
--- /dev/null
+++ b/.agents/agents/plugin-architect.md
@@ -0,0 +1,50 @@
+# Agent: Plugin Architect
+
+## Role
+
+You are a senior WordPress plugin architect at LightSpeed.
+You specialise in building maintainable, secure, accessible WordPress plugins — with a strong bias toward Gutenberg block development.
+
+---
+
+## Expertise
+
+- WordPress plugin architecture and best practices
+- Gutenberg block development (`block.json`, `@wordpress/scripts`, `register_block_type`)
+- PHP coding standards (WordPress Coding Standards, escaping, sanitisation)
+- JavaScript and React for the block editor
+- Accessibility (WCAG 2.1 AA, semantic HTML, ARIA)
+- Security (output escaping, input sanitisation, nonce verification, capability checks)
+- Performance (lean asset loading, no unnecessary dependencies)
+- LightSpeed plugin and theme scaffold conventions
+- AI-assisted development workflows
+
+---
+
+## Working style
+
+- Prefer small, precise diffs over large rewrites.
+- Keep the plugin lean — do not add dependencies or infrastructure that is not needed.
+- Follow WordPress core conventions before reaching for abstraction.
+- Always escape PHP output. Always sanitise PHP input. No exceptions.
+- Use `block.json` for block registration rather than bespoke PHP registration logic.
+- Reference `AGENTS.md` before making any changes to the repo.
+- Write reports to `.github/reports/` and tasks to `.github/tasks/`.
+- Do not modify unrelated files.
+- Explain your reasoning clearly when making architectural decisions.
+
+---
+
+## Available skills
+
+- [Block Plugin Audit](../skills/block-plugin-audit/SKILL.md)
+
+---
+
+## Guiding principles
+
+1. **WordPress-first** — use stable WordPress APIs before adding abstraction.
+2. **Security by default** — escape, sanitise, validate everywhere.
+3. **Accessible** — semantic HTML, ARIA, keyboard support in every block.
+4. **Lean** — every file, dependency, and line of code should earn its place.
+5. **Documented** — leave the repo in a better state than you found it.
diff --git a/.agents/skills/README.md b/.agents/skills/README.md
new file mode 100644
index 0000000..1f92aa0
--- /dev/null
+++ b/.agents/skills/README.md
@@ -0,0 +1,46 @@
+# .agents/skills
+
+This folder contains portable, reusable agent skills for {{PLUGIN_NAME}}.
+
+---
+
+## What is a skill?
+
+A skill is a focused, self-contained description of how an AI agent should perform a specific task in this repository.
+Skills are not personas — they describe *what to do*, not *who to be*.
+
+---
+
+## Structure
+
+Each skill lives in its own folder:
+
+```
+skills/
+└── {skill-name}/
+ └── SKILL.md
+```
+
+The `SKILL.md` file describes:
+- What the skill does
+- When to use it
+- Step-by-step instructions
+- Expected inputs and outputs
+- Quality criteria
+
+---
+
+## Available skills
+
+| Skill | Description |
+|---|---|
+| `block-plugin-audit/` | Audit a WordPress block plugin for quality, security, and accessibility |
+
+---
+
+## Adding new skills
+
+1. Create a folder under `.agents/skills/` with a descriptive name.
+2. Add a `SKILL.md` file following the structure above.
+3. Add it to the table in this README.
+4. Reference it from `AGENTS.md` if it is commonly needed.
diff --git a/.agents/skills/block-plugin-audit/SKILL.md b/.agents/skills/block-plugin-audit/SKILL.md
new file mode 100644
index 0000000..f99d71d
--- /dev/null
+++ b/.agents/skills/block-plugin-audit/SKILL.md
@@ -0,0 +1,87 @@
+# SKILL: Block Plugin Audit
+
+Audit a WordPress block plugin for code quality, security, and accessibility.
+
+---
+
+## When to use this skill
+
+Use this skill when:
+- Reviewing a plugin before a release.
+- Onboarding onto an existing plugin codebase.
+- Requested to produce a plugin audit report.
+- Running the `audit-plugin.prompt.md` prompt.
+
+---
+
+## Inputs
+
+- The plugin repository root directory.
+- Optional: specific files or folders to focus on.
+
+---
+
+## Steps
+
+### 1. Review plugin structure
+
+- Confirm the main plugin file exists and has a valid WordPress plugin header.
+- Confirm `defined( 'ABSPATH' )` direct access protection is present.
+- Confirm plugin constants are defined correctly.
+- Confirm includes are loaded via `plugins_loaded`.
+- Confirm the text domain matches the plugin slug.
+
+### 2. PHP security review
+
+- Scan all PHP files for unescaped output.
+- Check for unsanitised `$_GET`, `$_POST`, `$_REQUEST`, `$_COOKIE` usage.
+- Check for missing nonce verification on form submissions.
+- Check for missing capability checks before privileged operations.
+- Check for unsafe database queries (missing `$wpdb->prepare()`).
+- Flag any use of `eval()`, `exec()`, `system()`, or `shell_exec()`.
+
+### 3. Block quality review (if blocks present)
+
+- Confirm each block has a valid `block.json`.
+- Confirm blocks are registered using `register_block_type()` with `block.json`.
+- Confirm PHP render callbacks escape all dynamic output.
+- Confirm block patterns use safe escaping.
+- Confirm `block.json` files have `$schema`, `name`, `title`, `category`, and `textdomain`.
+
+### 4. Translation review
+
+- Confirm all user-facing strings use translation functions.
+- Confirm the correct text domain is used in all translation function calls.
+
+### 5. Accessibility review
+
+- Review block edit and save components for:
+ - Semantic HTML elements
+ - ARIA attributes where needed
+ - Keyboard navigation support
+ - Sufficient colour contrast (flag for manual review)
+- Review frontend-rendered output for accessibility markers.
+
+### 6. General quality
+
+- Check for unreplaced `{{PLACEHOLDER}}` tokens.
+- Check that `CHANGELOG.md` is up to date.
+- Check for unused files or stale commented-out code.
+
+---
+
+## Outputs
+
+1. Write an audit report to `.github/reports/audit-YYYY-MM-DD.md`.
+2. Create or update `.github/tasks/task-list.md` with actionable tasks from findings.
+
+---
+
+## Quality criteria
+
+A passing audit has:
+- No unescaped output in PHP files.
+- No unsanitised input used directly.
+- Valid `block.json` for every registered block.
+- No unreplaced placeholder tokens.
+- `CHANGELOG.md` reflects the current state of the plugin.
diff --git a/.coderabbit.yml b/.coderabbit.yml
new file mode 100644
index 0000000..0d7cef0
--- /dev/null
+++ b/.coderabbit.yml
@@ -0,0 +1,69 @@
+version: 2
+
+# Reviews are enabled and set to a draft-aware, plugin-focused review profile.
+reviews:
+ request_changes_workflow: false
+ high_level_summary: true
+ poem: false
+ review_status: true
+ collapse_walkthrough: false
+
+# Language and tone
+language: en-US
+tone_instructions: "Be direct and professional. Focus on practical WordPress plugin quality."
+
+# Path-level review instructions
+path_instructions:
+ - path: "{{PLUGIN_SLUG}}.php"
+ instructions: |
+ Review the main plugin bootstrap file.
+ Check for: valid plugin header with all required fields, direct access protection (ABSPATH check),
+ correct version constant, safe loading of includes, no business logic in the bootstrap.
+ Confirm text domain matches the plugin slug.
+
+ - path: "inc/**/*.php"
+ instructions: |
+ Review PHP include files.
+ Check for: correct escaping (esc_html__, esc_attr__, wp_kses_post, etc.),
+ sanitisation and validation of input, translation function usage, no direct database queries without $wpdb,
+ correct use of WordPress hooks and filters.
+
+ - path: "src/**/*.js"
+ instructions: |
+ Review JavaScript source files.
+ Check for: correct use of wp.element or vanilla JS, no jQuery dependency unless justified,
+ no hardcoded strings (use wp.i18n), accessibility-aware DOM interactions.
+
+ - path: "src/**/*.css"
+ instructions: |
+ Review CSS source files.
+ Check for: no use of !important unless justified, responsive-first approach,
+ use of CSS custom properties where appropriate, accessible colour contrast considerations.
+
+ - path: "blocks/**/block.json"
+ instructions: |
+ Review block registration JSON files.
+ Check for: valid schema reference, correct category and icon, editorScript/style/viewScript paths,
+ meaningful attributes, supports declarations, correct textdomain in titles and descriptions.
+
+ - path: "patterns/*.php"
+ instructions: |
+ Review block pattern PHP files.
+ Check for: correct escaping of any dynamic output, translation function usage,
+ pattern header comments (Title, Slug, Categories), clean markup.
+
+ - path: "AGENTS.md"
+ instructions: |
+ Review the AI agent guidance document.
+ Check for: clarity, completeness, placeholder consistency, accurate command references,
+ correct folder references for prompts, reports, tasks, docs, skills, and agents.
+
+ - path: "README.md"
+ instructions: |
+ Review the root README.
+ Check for: clear quick start, accurate command table, correct placeholder references,
+ sensible repo map, correct folder descriptions.
+
+# What CodeRabbit should focus on for this repo
+chat:
+ auto_reply: true
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..2eb2ebe
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,29 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.php]
+indent_style = tab
+indent_size = 4
+
+[*.{js,mjs,cjs,ts,jsx,tsx}]
+indent_style = space
+indent_size = 2
+
+[*.{css,scss}]
+indent_style = space
+indent_size = 2
+
+[*.{json,yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6b2edd3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,37 @@
+# Normalise line endings to LF on checkout
+* text=auto eol=lf
+
+# Binary files — do not modify
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.svg binary
+*.woff binary
+*.woff2 binary
+*.ttf binary
+*.eot binary
+*.zip binary
+
+# Export-ignore — exclude from dist archives
+.git export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.editorconfig export-ignore
+.nvmrc export-ignore
+.coderabbit.yml export-ignore
+.lintstagedrc.json export-ignore
+node_modules/ export-ignore
+vendor/ export-ignore
+src/ export-ignore
+.github/ export-ignore
+.agents/ export-ignore
+package.json export-ignore
+package-lock.json export-ignore
+composer.json export-ignore
+composer.lock export-ignore
+plugin-utils.mjs export-ignore
+AGENTS.md export-ignore
+CLAUDE.md export-ignore
+CODEOWNERS export-ignore
diff --git a/.github/README.md b/.github/README.md
new file mode 100644
index 0000000..d24d085
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,33 @@
+# .github
+
+This folder contains GitHub-native AI and repository workflow infrastructure for {{PLUGIN_NAME}}.
+
+---
+
+## Folder structure
+
+| Folder / File | Purpose |
+|---|---|
+| `copilot-instructions.md` | Primary GitHub Copilot instruction file for this repo |
+| `instructions/` | File-type-specific Copilot guidance files |
+| `prompts/` | Reusable GitHub Copilot prompt files for repeatable workflows |
+| `reports/` | Developer and AI-generated reports (not end-user docs) |
+| `tasks/` | Task lists and AI-maintained work tracking |
+| `workflows/` | GitHub Actions CI/CD workflows |
+
+---
+
+## Usage
+
+- AI agents: read `AGENTS.md` first, then `.github/copilot-instructions.md`.
+- Developers: see `prompts/` for useful starting prompts for common tasks.
+- Reports and task lists: see `reports/README.md` and `tasks/README.md`.
+- Workflows are triggered automatically via GitHub Actions.
+
+---
+
+## What does NOT belong in `.github/`
+
+- End-user documentation → `docs/`
+- Plugin source code → `src/`, `inc/`, `blocks/`
+- Static assets → `assets/`
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..6b506e1
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,86 @@
+# GitHub Copilot Instructions
+
+This file configures GitHub Copilot for the {{PLUGIN_NAME}} repository.
+Read [`AGENTS.md`](../AGENTS.md) for full agent guidance.
+
+---
+
+## Repo overview
+
+This is a LightSpeed WordPress block plugin starter.
+- One plugin, one repo.
+- Block-ready but not locked into a large block framework.
+- Uses placeholder tokens (`{{DOUBLE_BRACES}}`) throughout — replace before production.
+
+---
+
+## Key conventions
+
+- PHP: tabs for indentation, WordPress coding standards.
+- JS/CSS: spaces (2), follow `@wordpress/scripts` defaults.
+- Text domain must match the plugin slug everywhere.
+- All PHP output must be escaped. All input must be sanitised.
+- Use `esc_html__()`, `esc_attr__()`, `esc_url()`, `wp_kses_post()`.
+
+---
+
+## Where things live
+
+| What | Where |
+|---|---|
+| Main plugin bootstrap | `{{PLUGIN_SLUG}}.php` |
+| PHP includes | `inc/` |
+| Block source files | `src/blocks/` |
+| Built block assets | `blocks/` |
+| Static assets | `assets/` |
+| Block patterns | `patterns/` |
+| Translation files | `languages/` |
+| End-user docs | `docs/` |
+| Copilot prompts | `.github/prompts/` |
+| Developer reports | `.github/reports/` |
+| Task lists | `.github/tasks/` |
+| Portable skills | `.agents/skills/` |
+| Agent personas | `.agents/agents/` |
+
+---
+
+## File-type guidance
+
+See `.github/instructions/` for detailed guidance:
+
+- [`php.instructions.md`](instructions/php.instructions.md) — PHP coding standards and escaping
+- [`blocks.instructions.md`](instructions/blocks.instructions.md) — Block development conventions
+- [`plugin-structure.instructions.md`](instructions/plugin-structure.instructions.md) — Plugin structure rules
+- [`assets.instructions.md`](instructions/assets.instructions.md) — Asset conventions
+- [`workflows.instructions.md`](instructions/workflows.instructions.md) — CI/CD and workflow guidance
+
+---
+
+## Security reminders
+
+- Never echo unescaped dynamic values.
+- Always sanitise `$_GET`, `$_POST`, `$_REQUEST`, and `$_COOKIE`.
+- Use `$wpdb->prepare()` for database queries.
+- Validate nonces on form submissions.
+- Use capability checks before sensitive operations.
+
+---
+
+## Validation and linting
+
+```bash
+npm run plugin:validate # Validate plugin structure
+npm run security:scan # PHP security scan
+composer run phpcs # PHP coding standards
+npm run lint # JS + CSS + JSON linting
+```
+
+---
+
+## Accessibility
+
+- Use semantic HTML elements.
+- Add ARIA attributes where native semantics are insufficient.
+- Ensure sufficient colour contrast.
+- Support keyboard navigation in interactive blocks.
+- Use `aria-label` or `aria-labelledby` on interactive controls without visible labels.
diff --git a/.github/instructions/README.md b/.github/instructions/README.md
new file mode 100644
index 0000000..5551e5c
--- /dev/null
+++ b/.github/instructions/README.md
@@ -0,0 +1,26 @@
+# .github/instructions
+
+This folder contains file-type-specific GitHub Copilot instruction files for {{PLUGIN_NAME}}.
+
+---
+
+## Files
+
+| File | Purpose |
+|---|---|
+| `php.instructions.md` | PHP coding standards, escaping, and sanitisation |
+| `blocks.instructions.md` | WordPress block development conventions |
+| `plugin-structure.instructions.md` | Plugin folder structure and architecture rules |
+| `assets.instructions.md` | Asset organisation and build conventions |
+| `workflows.instructions.md` | CI/CD and GitHub Actions guidance |
+
+---
+
+## How these files work
+
+GitHub Copilot reads these files to understand context for specific file types or workflows.
+Reference them from `.github/copilot-instructions.md` using relative links.
+
+To add a new instruction file:
+1. Create a file named `{topic}.instructions.md` in this folder.
+2. Add a link to it from `.github/copilot-instructions.md`.
diff --git a/.github/instructions/assets.instructions.md b/.github/instructions/assets.instructions.md
new file mode 100644
index 0000000..2255193
--- /dev/null
+++ b/.github/instructions/assets.instructions.md
@@ -0,0 +1,50 @@
+---
+applyTo: "assets/**,src/**"
+---
+
+# Assets Instructions
+
+## Asset organisation
+
+| Folder | Content |
+|---|---|
+| `assets/css/` | Static (pre-built) CSS files for frontend or admin |
+| `assets/js/` | Static (pre-built) JS files not managed by block build |
+| `assets/images/` | Plugin images (logos, backgrounds, etc.) |
+| `assets/icons/` | SVG or PNG icons |
+| `src/blocks/` | Block source files — compiled by `@wordpress/scripts` |
+| `src/css/` | Non-block CSS source files |
+| `src/js/` | Non-block JS source files |
+| `blocks/` | Built block assets — output of `npm run build` |
+
+## Rules
+
+- Do not mix source and built files in the same folder.
+- `src/` contains files that need compilation.
+- `assets/` contains files that are already production-ready.
+- `blocks/` contains built block output from `@wordpress/scripts`.
+- Do not commit compiled output from `src/` — use `npm run build` to generate it.
+
+## Enqueuing assets in PHP
+
+Use `wp_enqueue_style()` and `wp_enqueue_script()` with versioning:
+
+```php
+wp_enqueue_style(
+ '{{PLUGIN_SLUG}}-frontend',
+ {{NAMESPACE}}_PLUGIN_URL . 'assets/css/frontend.css',
+ [],
+ {{NAMESPACE}}_VERSION
+);
+```
+
+## Block assets
+
+Block assets (editor and frontend CSS/JS) are declared in `block.json` and enqueued automatically by `register_block_type()`.
+Do not manually enqueue block scripts — let `block.json` handle it.
+
+## Image and icon guidelines
+
+- Optimise all images before committing.
+- Use SVG for icons where possible.
+- Do not commit large image files to the repository.
diff --git a/.github/instructions/blocks.instructions.md b/.github/instructions/blocks.instructions.md
new file mode 100644
index 0000000..6349a69
--- /dev/null
+++ b/.github/instructions/blocks.instructions.md
@@ -0,0 +1,96 @@
+---
+applyTo: "**/{blocks,src/blocks}/**"
+---
+
+# Blocks Instructions
+
+## Block structure
+
+Each block lives in its own directory under `src/blocks/{{block-name}}/`.
+Built assets are output to `blocks/{{block-name}}/`.
+
+Typical block directory:
+
+```
+src/blocks/my-block/
+├── block.json Block registration metadata
+├── edit.js Block editor component
+├── index.js Block registration entry point
+├── editor.css Editor-only styles (optional)
+└── style.css Frontend styles (optional)
+```
+
+## block.json
+
+Every block must have a `block.json` file with:
+
+- `$schema`: reference the WordPress block schema
+- `apiVersion`: use `3`
+- `name`: `{{PLUGIN_SLUG}}/block-name`
+- `title`, `description`, `category`, `icon`
+- `textdomain`: `{{TEXT_DOMAIN}}`
+- `editorScript`, `style`, `viewScript` as appropriate
+- `supports` and `attributes` declarations
+
+Example:
+
+```json
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "{{PLUGIN_SLUG}}/my-block",
+ "title": "My Block",
+ "category": "text",
+ "icon": "block-default",
+ "description": "A short block description.",
+ "textdomain": "{{TEXT_DOMAIN}}",
+ "editorScript": "file:./index.js",
+ "style": "file:./style.css",
+ "supports": {
+ "html": false,
+ "color": { "background": true, "text": true }
+ }
+}
+```
+
+## Registration in PHP
+
+Register blocks in the main plugin file or a dedicated `inc/` file:
+
+```php
+register_block_type( {{NAMESPACE}}_PLUGIN_DIR . 'blocks/my-block' );
+```
+
+Do not manually register scripts and styles when `block.json` handles it.
+
+## PHP-rendered blocks
+
+For `render_callback` blocks:
+
+```php
+function {{PLUGIN_SLUG}}_render_my_block( $attributes, $content, $block ) {
+ $title = isset( $attributes['title'] ) ? sanitize_text_field( $attributes['title'] ) : '';
+ return '
' . esc_html( $title ) . '
';
+}
+```
+
+Always escape output. Always sanitise attributes before use.
+
+## Editor vs frontend assets
+
+- Editor CSS: `editorStyle` in `block.json`
+- Frontend CSS: `style` in `block.json`
+- Editor JS: `editorScript` in `block.json`
+- Frontend JS (interactive): `viewScript` in `block.json`
+
+## Build tooling
+
+Use `@wordpress/scripts` for building block assets:
+
+```bash
+npm run build # Production build
+npm run start # Development watch
+```
+
+Block source: `src/blocks/`
+Block output: `blocks/`
diff --git a/.github/instructions/php.instructions.md b/.github/instructions/php.instructions.md
new file mode 100644
index 0000000..1caa965
--- /dev/null
+++ b/.github/instructions/php.instructions.md
@@ -0,0 +1,61 @@
+---
+applyTo: "**/*.php"
+---
+
+# PHP Instructions
+
+## Coding standards
+
+- Follow [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/).
+- Use **tabs** for indentation (not spaces).
+- Opening braces go on the same line for control structures.
+- Use `true`, `false`, `null` (lowercase).
+
+## Escaping output
+
+Always escape before output. Match the escaping function to the context:
+
+```php
+echo esc_html( $text ); // Plain text output
+echo esc_attr( $attribute ); // HTML attribute values
+echo esc_url( $url ); // URLs in href/src
+echo esc_html__( 'String', '{{TEXT_DOMAIN}}' ); // Translated plain text
+echo wp_kses_post( $html ); // HTML content (limited tags)
+```
+
+Never echo unescaped variables or user input.
+
+## Sanitising input
+
+Sanitise all external input before storing or using it:
+
+```php
+$text = sanitize_text_field( wp_unslash( $_POST['field'] ) );
+$url = esc_url_raw( wp_unslash( $_POST['url'] ) );
+$int = absint( $_POST['number'] );
+$email = sanitize_email( $_POST['email'] );
+$html = wp_kses_post( wp_unslash( $_POST['content'] ) );
+```
+
+## Translation
+
+Wrap all user-facing strings:
+
+```php
+esc_html__( 'String', '{{TEXT_DOMAIN}}' )
+esc_html_e( 'String', '{{TEXT_DOMAIN}}' )
+esc_attr__( 'String', '{{TEXT_DOMAIN}}' )
+```
+
+## Security
+
+- Check nonces with `check_ajax_referer()` or `wp_verify_nonce()` before processing form submissions.
+- Check capabilities with `current_user_can()` before privileged operations.
+- Use `$wpdb->prepare()` for all database queries with dynamic values.
+- Never use `eval()`.
+
+## File structure
+
+- PHP includes go in `inc/`.
+- Class files: `inc/class-{{plugin-slug}}-name.php`.
+- Load includes from the main plugin file via `plugins_loaded`.
diff --git a/.github/instructions/plugin-structure.instructions.md b/.github/instructions/plugin-structure.instructions.md
new file mode 100644
index 0000000..d2c07cb
--- /dev/null
+++ b/.github/instructions/plugin-structure.instructions.md
@@ -0,0 +1,56 @@
+---
+applyTo: "**"
+---
+
+# Plugin Structure Instructions
+
+## Root files
+
+| File | Purpose |
+|---|---|
+| `{{PLUGIN_SLUG}}.php` | Main plugin bootstrap — valid WordPress plugin header required |
+| `uninstall.php` | Plugin uninstall handler — keep conservative, no data deletion by default |
+| `plugin-utils.mjs` | Plugin validation and utility CLI |
+| `package.json` | Node scripts and dev dependencies |
+| `composer.json` | PHP quality tooling |
+| `AGENTS.md` | AI agent guidance — read this first |
+| `CHANGELOG.md` | Version history — follow Keep a Changelog + SemVer |
+| `README.md` | Human-readable root documentation |
+
+## Main plugin file rules
+
+- Must contain a valid WordPress plugin header.
+- Must include `defined( 'ABSPATH' ) || exit;` direct access protection.
+- Define plugin constants: `{{NAMESPACE}}_VERSION`, `{{NAMESPACE}}_PLUGIN_DIR`, `{{NAMESPACE}}_PLUGIN_URL`.
+- Load includes via `plugins_loaded` — not at the top level.
+- Keep the bootstrap lean — no business logic.
+
+## Directory roles
+
+| Directory | Purpose |
+|---|---|
+| `inc/` | Optional PHP include files — loaded from main plugin file |
+| `src/` | Source files for compilation (blocks, CSS, JS) |
+| `blocks/` | Built block assets — output of `npm run build` |
+| `assets/` | Static (pre-built) CSS, JS, images, icons |
+| `patterns/` | WordPress block patterns (PHP with header comments) |
+| `templates/` | Optional block templates and template parts |
+| `languages/` | Translation files (.pot, .po, .mo) |
+| `docs/` | End-user documentation only |
+| `.github/` | GitHub-native workflows, instructions, prompts, reports, tasks |
+| `.agents/` | Portable agent skills and personas |
+
+## Placeholder consistency
+
+All `{{PLACEHOLDER}}` tokens must be replaced consistently:
+- Text domain = plugin slug everywhere.
+- Namespace = consistent uppercase constant prefix.
+- Package name = consistent in package.json and composer.json.
+
+## What NOT to do
+
+- Do not put developer reports in `docs/`.
+- Do not put built assets in `src/`.
+- Do not add Playwright, Storybook, Docker, webpack config, or Vite.
+- Do not add a PHP autoloader unless there is a genuine reason.
+- Do not add issue templates or pull request templates.
diff --git a/.github/instructions/workflows.instructions.md b/.github/instructions/workflows.instructions.md
new file mode 100644
index 0000000..8d8013f
--- /dev/null
+++ b/.github/instructions/workflows.instructions.md
@@ -0,0 +1,54 @@
+---
+applyTo: ".github/workflows/**"
+---
+
+# Workflows Instructions
+
+## GitHub Actions workflows
+
+| Workflow | Purpose |
+|---|---|
+| `ci.yml` | Install dependencies, validate plugin, lint |
+| `code-quality.yml` | PHP coding standards and lint checks |
+| `release.yml` | Validate plugin scaffold (plugin:validate, security scan, placeholder scan, PHPCS) on release tags |
+
+## Rules
+
+- Keep workflows focused and fast.
+- Install Node dependencies with `npm ci`.
+- Install Composer dependencies with `composer install --no-interaction`.
+- Run `npm run plugin:validate` as part of CI.
+- Run `composer run phpcs` as part of CI.
+- Do not add Docker, Playwright, or E2E tests unless explicitly required.
+
+## Workflow triggers
+
+- `ci.yml` and `code-quality.yml` should run on `push` and `pull_request` to `main`.
+- `release.yml` should run on `push` of tags matching `v*`.
+
+## Secrets
+
+- Do not hardcode credentials in workflow files.
+- Use GitHub Actions secrets for any tokens or keys.
+
+## Node setup
+
+Use the `actions/setup-node` action with `.nvmrc` for the Node version:
+
+```yaml
+- uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+```
+
+## Composer setup
+
+Use the `shivammathur/setup-php` action for PHP:
+
+```yaml
+- uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ tools: composer
+```
diff --git a/.github/prompts/README.md b/.github/prompts/README.md
new file mode 100644
index 0000000..bf44e26
--- /dev/null
+++ b/.github/prompts/README.md
@@ -0,0 +1,41 @@
+# .github/prompts
+
+This folder contains GitHub Copilot prompt files for {{PLUGIN_NAME}}.
+These are reusable starting points for common development workflows.
+
+---
+
+## Files
+
+| File | Purpose |
+|---|---|
+| `cleanup.prompt.md` | Clean up and review a PHP or JS file |
+| `audit-plugin.prompt.md` | Audit the full plugin for quality issues |
+| `new-block.prompt.md` | Scaffold a new Gutenberg block |
+| `release.prompt.md` | Prepare a plugin release |
+
+---
+
+## How to use prompts
+
+1. Open GitHub Copilot Chat in VS Code.
+2. Type `/` to see available prompt files, or reference them directly.
+3. Provide any additional context the prompt asks for.
+
+---
+
+## Adding new prompts
+
+1. Create a file named `{workflow}.prompt.md` in this folder.
+2. Use Markdown with a clear title and instructions.
+3. Reference specific files or patterns relevant to this plugin.
+4. Add it to the table above.
+
+---
+
+## Notes
+
+- Prompts are GitHub-specific and live in `.github/prompts/`.
+- Portable, reusable skills live in `.agents/skills/`.
+- Reports generated by prompts should go in `.github/reports/`.
+- Task lists generated by prompts should go in `.github/tasks/`.
diff --git a/.github/prompts/audit-plugin.prompt.md b/.github/prompts/audit-plugin.prompt.md
new file mode 100644
index 0000000..586943f
--- /dev/null
+++ b/.github/prompts/audit-plugin.prompt.md
@@ -0,0 +1,40 @@
+# Audit Plugin Prompt
+
+Perform a quality and security audit of this WordPress plugin.
+
+## Instructions
+
+Review the following areas and produce a structured report:
+
+### 1. Plugin structure
+- Is the main plugin file correctly structured with a valid header?
+- Is ABSPATH protection present?
+- Are constants defined correctly?
+- Are includes loaded correctly via `plugins_loaded`?
+
+### 2. PHP quality
+- Is all output correctly escaped?
+- Is all input correctly sanitised and validated?
+- Are nonces used on form submissions?
+- Are capability checks in place before privileged operations?
+- Are translation functions used for all user-facing strings?
+
+### 3. Block quality (if blocks are present)
+- Is each block registered with a valid `block.json`?
+- Is `block.json` used for asset declaration instead of manual enqueuing?
+- Are PHP-rendered block callbacks escaping all output?
+- Are block patterns using safe escaping?
+
+### 4. JavaScript quality
+- Are there any hardcoded strings that should be translated?
+- Are there any obvious accessibility issues?
+
+### 5. General
+- Are any `{{PLACEHOLDER}}` tokens still unreplaced?
+- Is the `CHANGELOG.md` up to date?
+- Are there any unused files or folders?
+
+## Output format
+
+Write the audit report to `.github/reports/audit-YYYY-MM-DD.md`.
+Create a task list in `.github/tasks/task-list.md` based on findings.
diff --git a/.github/prompts/cleanup.prompt.md b/.github/prompts/cleanup.prompt.md
new file mode 100644
index 0000000..5638e4e
--- /dev/null
+++ b/.github/prompts/cleanup.prompt.md
@@ -0,0 +1,25 @@
+# Cleanup Prompt
+
+Review the specified file and clean it up.
+
+## Instructions
+
+1. Review the file for:
+ - Unused variables, functions, or imports
+ - Dead or commented-out code that should be removed
+ - Inconsistent indentation or formatting
+ - Missing or outdated doc comments
+ - Strings not wrapped in translation functions
+ - PHP output that is not escaped
+
+2. Apply fixes using the project's coding standards:
+ - PHP: WordPress Coding Standards (tabs, WordPress escaping, translation)
+ - JS: `@wordpress/scripts` eslint config
+ - CSS: `@wordpress/scripts` stylelint config
+
+3. Do not change logic — only clean up formatting, escaping, and documentation.
+
+## File to review
+
+> Replace this line with the path to the file you want to clean up.
+> Example: `inc/class-my-plugin-example.php`
diff --git a/.github/prompts/new-block.prompt.md b/.github/prompts/new-block.prompt.md
new file mode 100644
index 0000000..cabccf7
--- /dev/null
+++ b/.github/prompts/new-block.prompt.md
@@ -0,0 +1,37 @@
+# New Block Prompt
+
+Scaffold a new Gutenberg block for this plugin.
+
+## Instructions
+
+Create a new block with the following details:
+
+- **Block slug**: (replace with your block name, e.g. `my-block`)
+- **Block title**: (replace with a human-readable title)
+- **Block description**: (replace with a short description)
+- **Block category**: (e.g. `text`, `media`, `design`, `theme`, `widgets`)
+- **Block type**: static (uses Save component) or dynamic (uses PHP render callback)
+
+## What to create
+
+1. `src/blocks/{{block-slug}}/block.json` — block registration metadata
+2. `src/blocks/{{block-slug}}/index.js` — block registration entry point
+3. `src/blocks/{{block-slug}}/edit.js` — block editor component
+4. `src/blocks/{{block-slug}}/save.js` — block save function (for static blocks)
+5. `src/blocks/{{block-slug}}/style.css` — frontend styles
+6. For dynamic blocks: PHP render callback registered in `inc/` or the main plugin file
+
+## Rules
+
+- Use `block.json` for all asset declarations — no manual `wp_enqueue_*` for block assets.
+- Escape all PHP output in render callbacks.
+- Use `{{TEXT_DOMAIN}}` as the text domain for all translated strings.
+- Block name must be `{{PLUGIN_SLUG}}/{{block-slug}}`.
+- Run `npm run build` after creating source files.
+- Register the block in the main plugin file using `register_block_type()`.
+
+## After creation
+
+- Run `npm run schema:validate` to validate `block.json`.
+- Run `npm run build` to compile block assets.
+- Update `CHANGELOG.md` under `[Unreleased]`.
diff --git a/.github/prompts/release.prompt.md b/.github/prompts/release.prompt.md
new file mode 100644
index 0000000..7e0f326
--- /dev/null
+++ b/.github/prompts/release.prompt.md
@@ -0,0 +1,49 @@
+# Release Prompt
+
+Prepare a new plugin release.
+
+## Instructions
+
+Perform the following steps to prepare a release:
+
+### 1. Review changes
+- Review `git log` or `git diff` since the last release.
+- Identify all significant changes (features, fixes, security updates).
+
+### 2. Update CHANGELOG.md
+- Move all entries from `[Unreleased]` to a new versioned section.
+- Use the format `## [X.Y.Z] - YYYY-MM-DD`.
+- Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) conventions.
+- Add a compare link at the bottom of `CHANGELOG.md`.
+
+### 3. Update version numbers
+Update the version number in:
+- `{{PLUGIN_SLUG}}.php` — `Version:` header field
+- `{{PLUGIN_SLUG}}.php` — `{{NAMESPACE}}_VERSION` constant
+- `package.json` — `version` field
+- `readme.txt` — `Stable tag:` field
+
+### 4. Validate
+Run:
+```bash
+npm run plugin:validate
+npm run security:scan
+composer run phpcs
+npm run lint
+```
+
+Fix any issues before tagging.
+
+### 5. Commit and tag
+```bash
+git add .
+git commit -m "Release v{VERSION}"
+git tag v{VERSION}
+git push && git push --tags
+```
+
+## Notes
+
+- Follow [SemVer](https://semver.org/): MAJOR.MINOR.PATCH.
+- Security fixes should be PATCH or MINOR releases with a `Security` changelog entry.
+- Do not release with unreplaced `{{PLACEHOLDER}}` tokens.
diff --git a/.github/reports/.gitkeep b/.github/reports/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/.github/reports/README.md b/.github/reports/README.md
new file mode 100644
index 0000000..d4ef122
--- /dev/null
+++ b/.github/reports/README.md
@@ -0,0 +1,66 @@
+# .github/reports
+
+This folder contains **developer and AI-generated reports** for {{PLUGIN_NAME}}.
+
+---
+
+## What belongs here
+
+- Plugin audit reports
+- Security scan reports
+- Code quality reports
+- AI-generated analysis outputs
+- Accessibility audit reports
+
+## What does NOT belong here
+
+- End-user documentation → `docs/`
+- Task lists → `.github/tasks/`
+
+---
+
+## Naming conventions
+
+Use the following format for report filenames:
+
+```
+{type}-YYYY-MM-DD.md
+```
+
+Examples:
+- `audit-2025-06-01.md`
+- `security-scan-2025-06-01.md`
+- `accessibility-audit-2025-07-15.md`
+
+For multiple reports of the same type in one period, append a short descriptor:
+
+```
+audit-2025-06-01-blocks.md
+audit-2025-06-01-php.md
+```
+
+---
+
+## Creating reports
+
+When an AI agent runs a prompt from `.github/prompts/audit-plugin.prompt.md` or similar:
+1. Write the report to this folder using the naming convention above.
+2. Summarise findings clearly under headings.
+3. Create follow-up tasks in `.github/tasks/task-list.md`.
+
+---
+
+## Subfolders (optional)
+
+For larger repos, reports may be organised by month or category:
+
+```
+reports/
+├── 2025-06/
+│ ├── audit-2025-06-01.md
+│ └── security-scan-2025-06-15.md
+└── 2025-07/
+ └── audit-2025-07-01.md
+```
+
+This is optional — keep it flat if the volume is low.
diff --git a/.github/tasks/.gitkeep b/.github/tasks/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/.github/tasks/README.md b/.github/tasks/README.md
new file mode 100644
index 0000000..970553d
--- /dev/null
+++ b/.github/tasks/README.md
@@ -0,0 +1,53 @@
+# .github/tasks
+
+This folder contains task lists and AI-maintained work tracking for {{PLUGIN_NAME}}.
+
+---
+
+## Files
+
+| File | Purpose |
+|---|---|
+| `task-list.md` | Current task list — update as work progresses |
+| `.gitkeep` | Keeps the folder tracked by Git when empty |
+
+---
+
+## Naming conventions
+
+| File type | Format | Example |
+|---|---|---|
+| Main task list | `task-list.md` | `task-list.md` |
+| Milestone task list | `tasks-{milestone}.md` | `tasks-v1.0.0.md` |
+| Sprint task list | `tasks-{date}.md` | `tasks-2025-06.md` |
+
+---
+
+## When to create task lists
+
+- After running an audit prompt — create tasks from findings.
+- When planning a new feature or milestone.
+- When an AI agent identifies work that requires human review.
+
+---
+
+## How AI agents should use this folder
+
+- After generating a report in `.github/reports/`, create or update `task-list.md`.
+- Mark tasks as `[ ]` (pending) or `[x]` (completed).
+- Add a reference to the report that generated each task.
+- Do not delete completed tasks — mark them done so there is a history.
+
+---
+
+## How tasks relate to reports
+
+- Reports live in `.github/reports/`.
+- Tasks live in `.github/tasks/`.
+- A task entry should reference the report it came from where applicable.
+
+Example:
+
+```markdown
+- [ ] Fix unescaped output in `inc/class-example.php:42` — see `reports/audit-2025-06-01.md`
+```
diff --git a/.github/tasks/task-list.md b/.github/tasks/task-list.md
new file mode 100644
index 0000000..d6f46f5
--- /dev/null
+++ b/.github/tasks/task-list.md
@@ -0,0 +1,24 @@
+# Task List
+
+Active tasks for {{PLUGIN_NAME}}.
+Update this file as work progresses.
+
+---
+
+## In progress
+
+
+
+## Pending
+
+- [ ] Replace all `{{PLACEHOLDER}}` tokens with real values
+- [ ] Rename `{{PLUGIN_SLUG}}.php` to match the actual plugin slug
+- [ ] Update `CHANGELOG.md` with the actual release date
+- [ ] Update `CODEOWNERS` with real GitHub usernames
+- [ ] Update `docs/README.md` with the plugin's documentation structure
+- [ ] Run `npm run plugin:validate` and resolve any warnings
+- [ ] Install dependencies: `npm install && composer install`
+
+## Completed
+
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3d9a7c7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,40 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ validate:
+ name: Validate plugin
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install Node dependencies
+ run: npm ci
+
+ - name: Validate plugin structure
+ run: npm run plugin:validate
+
+ - name: Validate schema
+ run: npm run schema:validate
+
+ - name: Security scan
+ run: npm run security:scan
+
+ - name: Lint JS and CSS
+ run: npm run lint
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
new file mode 100644
index 0000000..8c684ac
--- /dev/null
+++ b/.github/workflows/code-quality.yml
@@ -0,0 +1,35 @@
+name: Code Quality
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ php-quality:
+ name: PHP code quality
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ tools: composer
+ coverage: none
+
+ - name: Install Composer dependencies
+ run: composer install --no-interaction --prefer-dist --no-progress
+
+ - name: PHP syntax check
+ run: composer run phplint
+
+ - name: PHP coding standards
+ run: composer run phpcs
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..27c3cea
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,49 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: read
+
+jobs:
+ release:
+ name: Validate release
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Install Node dependencies
+ run: npm ci
+
+ - name: Validate plugin structure
+ run: npm run plugin:validate
+
+ - name: Security scan
+ run: npm run security:scan
+
+ - name: Check for unreplaced placeholders
+ run: node plugin-utils.mjs scan-placeholders
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ tools: composer
+ coverage: none
+
+ - name: Install Composer dependencies
+ run: composer install --no-interaction --prefer-dist --no-progress
+
+ - name: PHP coding standards
+ run: composer run phpcs
diff --git a/.gitignore b/.gitignore
index 5469669..05901a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,48 +1,55 @@
-# Wordpress - ignore core, configuration, examples, uploads and logs.
-# https://github.com/github/gitignore/blob/main/WordPress.gitignore
-
-# Core
-#
-# Note: if you want to stage/commit WP core files
-# you can delete this whole section/until Configuration.
-/wp-admin/
-/wp-content/index.php
-/wp-content/languages
-/wp-content/plugins/index.php
-/wp-content/themes/index.php
-/wp-includes/
-/index.php
-/license.txt
-/readme.html
-/wp-*.php
-/xmlrpc.php
-
-# Configuration
-wp-config.php
-
-# Example themes
-/wp-content/themes/twenty*/
-
-# Example plugin
-/wp-content/plugins/hello.php
-
-# Uploads
-/wp-content/uploads/
-
-# Log files
-*.log
-
-# htaccess
-/.htaccess
+# Dependencies
+node_modules/
+vendor/
-# All plugins
-#
-# Note: If you wish to whitelist plugins,
-# uncomment the next line
-#/wp-content/plugins
+# Build output
+build/
+dist/
-# All themes
-#
-# Note: If you wish to whitelist themes,
-# uncomment the next line
-#/wp-content/themes
\ No newline at end of file
+# Logs
+*.log
+logs/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Editor
+.vscode/settings.json
+.idea/
+*.swp
+*.swo
+*~
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# Archives
+*.zip
+*.tar.gz
+*.tgz
+
+# PHP
+.php-cs-fixer.cache
+.phpunit.cache/
+.phpunit.result.cache
+
+# Node
+.npm/
+.eslintcache
+.stylelintcache
+
+# Temp
+tmp/
+temp/
+
+# AI / local cache
+.cache/
+.ai-cache/
+
+# Lock files
+# package-lock.json is committed to lock Node dependencies.
+# composer.lock is NOT committed — this is a plugin, not a deployment artefact.
+composer.lock
\ No newline at end of file
diff --git a/.lintstagedrc.json b/.lintstagedrc.json
new file mode 100644
index 0000000..2637421
--- /dev/null
+++ b/.lintstagedrc.json
@@ -0,0 +1,7 @@
+{
+ "*.php": ["composer run phpcs --", "composer run phplint --"],
+ "src/**/*.js": ["npm run lint:js --"],
+ "src/**/*.css": ["npm run lint:css --"],
+ "blocks/**/block.json": ["npm run lint:json --"],
+ "*.json": ["npm run lint:json --"]
+}
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..209e3ef
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+20
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..efa3784
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,227 @@
+# AGENTS.md — AI Agent Guidance
+
+This is the primary guidance document for AI agents working in this repository.
+Read this file first before making any changes.
+
+---
+
+## Repo Purpose
+
+This is a LightSpeed WordPress block plugin starter repository.
+It provides a clean, lean scaffold for building custom WordPress plugins — with a bias toward Gutenberg block development.
+
+It is **not** a WordPress.org submission starter.
+It is **not** a monorepo.
+It is a **single plugin in a single repo**.
+
+---
+
+## Repo Structure
+
+```
+/
+├── {{PLUGIN_SLUG}}.php Main plugin bootstrap — rename to match your slug
+├── uninstall.php Plugin uninstall stub — keep conservative
+├── plugin-utils.mjs Node CLI for validation, schema checks, and security scanning
+├── package.json Node scripts and dev dependencies
+├── composer.json PHP quality tooling
+├── AGENTS.md This file — start here
+├── CLAUDE.md Claude agent pointer
+├── CHANGELOG.md Version history — follow Keep a Changelog + SemVer
+├── README.md Human-readable root documentation
+├── readme.txt Lightweight WordPress distribution placeholder
+├── inc/ PHP include files (optional, loaded from main plugin file)
+├── src/ Source files: src/blocks/, src/css/, src/js/
+├── blocks/ Built or registered block asset directories
+├── assets/ Static assets: assets/css/, assets/js/, assets/images/, assets/icons/
+├── patterns/ WordPress block patterns (PHP files with header comments)
+├── templates/ Optional block templates and template parts
+├── languages/ Translation files (.pot, .po, .mo)
+├── docs/ End-user documentation — NOT developer reports
+├── .github/ GitHub-native workflows, Copilot instructions, prompts, tasks, reports
+└── .agents/ Portable agent skills and personas
+```
+
+---
+
+## Plugin-First Development
+
+- This is a **plugin repo** — not a theme, not a monorepo.
+- One plugin, one repo. Do not add multi-plugin infrastructure.
+- The main plugin file must maintain a valid WordPress plugin header.
+- Keep the bootstrap file lean — load includes via `plugins_loaded`, not inline.
+- Do not invent a plugin framework. Use WordPress core APIs.
+
+---
+
+## Placeholder Conventions
+
+All placeholder tokens use `{{DOUBLE_BRACES}}` format.
+These must be replaced before the plugin is used in production.
+
+| Placeholder | Purpose |
+|---|---|
+| `{{PLUGIN_NAME}}` | Human-readable plugin name |
+| `{{PLUGIN_SLUG}}` | URL-safe slug (lowercase, hyphens) |
+| `{{TEXT_DOMAIN}}` | WordPress text domain — must match slug |
+| `{{PLUGIN_URI}}` | Plugin URI |
+| `{{PLUGIN_DESCRIPTION}}` | One-sentence plugin description |
+| `{{AUTHOR_NAME}}` | Author or organisation name |
+| `{{AUTHOR_URI}}` | Author website URL |
+| `{{NAMESPACE}}` | PHP constant namespace prefix (uppercase, underscores) |
+| `{{PACKAGE_NAME}}` | Composer/npm package name |
+| `{{REPO_NAME}}` | GitHub repository name |
+| `{{GITHUB_ORG}}` | GitHub organisation name |
+
+**Rules:**
+- Text domain and slug must always match.
+- Namespace must be uppercase and underscore-separated.
+- Do not leave required metadata blank — use placeholders.
+- Keep placeholders consistent across all files.
+
+---
+
+## PHP Architecture
+
+- Keep PHP minimal unless there is a clear need.
+- PHP include files go in `inc/`.
+- Load includes from `{{PLUGIN_SLUG}}_init()` via `plugins_loaded`.
+- Use WordPress coding standards (tabs for indentation in PHP).
+- Do not add a PHP autoloader unless genuinely needed.
+- Class files go in `inc/` — name them `class-{{plugin-slug}}-name.php`.
+
+### Escaping output (required)
+
+Always escape output before rendering:
+
+```php
+echo esc_html( $variable );
+echo esc_attr( $attribute );
+echo esc_url( $url );
+echo wp_kses_post( $html );
+```
+
+Never echo unescaped dynamic content.
+
+### Sanitising input (required)
+
+Sanitise all user input before using it:
+
+```php
+$value = sanitize_text_field( wp_unslash( $_POST['field'] ) );
+$url = esc_url_raw( wp_unslash( $_POST['url'] ) );
+$int = absint( $_POST['number'] );
+```
+
+### Translation
+
+Always wrap strings for translation:
+
+```php
+esc_html__( 'String', '{{TEXT_DOMAIN}}' )
+esc_html_e( 'String', '{{TEXT_DOMAIN}}' )
+```
+
+---
+
+## Block Development
+
+- Source block files go in `src/blocks/{{block-name}}/`.
+- Built block assets go in `blocks/{{block-name}}/`.
+- Each block must have a valid `block.json` file.
+- Register blocks using `register_block_type()` with the path to `block.json`.
+- Use `@wordpress/scripts` for building block assets.
+- Block editor assets should be separate from frontend assets.
+- PHP-rendered blocks must escape all dynamic output.
+- Use `block.json` `supports` and `attributes` rather than bespoke registration logic.
+
+Example block registration:
+
+```php
+register_block_type( {{NAMESPACE}}_PLUGIN_DIR . 'blocks/my-block' );
+```
+
+---
+
+## Asset Conventions
+
+- **Static assets** (ready-to-use CSS, JS, images, icons): `assets/`
+- **Source files** (need compilation): `src/`
+- **Built block assets**: `blocks/`
+- Do not mix source and built assets in the same folder.
+- Frontend CSS and editor CSS should be separate files where appropriate.
+
+---
+
+## Validation and Linting Commands
+
+Run these before committing:
+
+```bash
+# Node validation
+npm run plugin:validate # Validate plugin structure and headers
+npm run schema:validate # Validate block.json and JSON files
+npm run security:scan # Scan for risky PHP patterns
+npm run lint # Run all JS, CSS, and JSON linters
+
+# PHP quality
+composer run phpcs # Check coding standards
+composer run phpcbf # Auto-fix coding standards
+composer run phplint # Check PHP syntax
+```
+
+---
+
+## Changelog Expectations
+
+- Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+- Follow [Semantic Versioning](https://semver.org/).
+- Every version bump must include a `CHANGELOG.md` entry.
+- Keep an `[Unreleased]` section at the top.
+- Use sections: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
+
+---
+
+## Documentation Expectations
+
+- `docs/` is for **end-user documentation** — installation guides, editor guides, client notes.
+- Do **not** put developer reports in `docs/`.
+- Developer reports go in `.github/reports/`.
+- Task lists go in `.github/tasks/`.
+
+---
+
+## AI Folder Conventions
+
+| Folder | Purpose |
+|---|---|
+| `.github/prompts/` | GitHub Copilot prompt files for repeatable workflows |
+| `.github/reports/` | Developer and AI-generated reports for this repo |
+| `.github/tasks/` | Task lists and AI-maintained work tracking |
+| `.github/instructions/` | File-type-specific Copilot guidance |
+| `.agents/skills/` | Portable, reusable agent skills |
+| `.agents/agents/` | Agent persona definitions |
+
+**Rules for AI agents:**
+- Store reports in `.github/reports/` — never in `docs/` or the repo root.
+- Store task lists in `.github/tasks/` — update `task-list.md` when work is tracked.
+- Store GitHub prompt files in `.github/prompts/`.
+- Store portable skills in `.agents/skills/`.
+- Store agent personas in `.agents/agents/`.
+- Keep prompts focused and short — they are wrappers for repeatable workflows.
+
+---
+
+## Code Quality Rules For AI Agents
+
+- Prefer **small diffs** — avoid large rewrites unless clearly justified.
+- Avoid adding new dependencies unless strictly necessary.
+- Do not invent a build pipeline — use `@wordpress/scripts` if builds are needed.
+- Keep the plugin lean — do not add infrastructure that is not used.
+- Do not add Playwright, Storybook, Docker, webpack config, or Vite unless there is a strong reason.
+- Escape all PHP output correctly.
+- Sanitise and validate all inputs.
+- Review block render callbacks and patterns carefully before modifying.
+- Do not hardcode plugin-specific values — use constants and placeholders.
+- Follow WordPress coding standards.
+- Follow accessibility best practices in blocks and templates.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0f5c3ed
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,40 @@
+# Changelog
+
+All notable changes to this plugin will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+---
+
+## [Unreleased]
+
+### Added
+
+### Changed
+
+### Deprecated
+
+### Removed
+
+### Fixed
+
+### Security
+
+---
+
+## [0.1.0] - YYYY-MM-DD
+
+### Added
+- Initial plugin scaffold with placeholder structure.
+- Block-ready `src/blocks/` and `blocks/` folder layout.
+- `plugin-utils.mjs` for plugin validation, schema checks, and security scanning.
+- Composer-based PHP quality tooling (PHPCS, PHPCBF, PHP lint).
+- `.github/` folder with Copilot instructions, prompts, reports, tasks, and workflows.
+- `.agents/` folder with portable skills and agent personas.
+- `docs/` folder for end-user documentation.
+
+---
+
+[Unreleased]: https://github.com/{{GITHUB_ORG}}/{{REPO_NAME}}/compare/v0.1.0...HEAD
+[0.1.0]: https://github.com/{{GITHUB_ORG}}/{{REPO_NAME}}/releases/tag/v0.1.0
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..c577915
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,5 @@
+# CLAUDE.md
+
+This repository uses `AGENTS.md` as the primary AI agent guidance document.
+
+Please read [`AGENTS.md`](./AGENTS.md) before making any changes to this repository.
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000..7076ddb
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,4 @@
+# {{GITHUB_ORG}}/{{REPO_NAME}}
+
+# Default code owners — update with real GitHub usernames.
+* @{{GITHUB_ORG}}/{{REPO_NAME}}-team
diff --git a/README.md b/README.md
index 91dc910..b13cecd 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,191 @@
-# ls-starter-plugin
\ No newline at end of file
+# {{PLUGIN_NAME}}
+
+> LightSpeed WordPress block plugin starter — lean, block-ready, AI-workflow-aware.
+
+---
+
+## What this repo is
+
+A production-aware starter template for building custom WordPress plugins at LightSpeed.
+It is **not** a WordPress.org submission starter and does not include submission-specific bureaucracy.
+
+It is intentionally lighter than `block-plugin-scaffold` while still providing:
+
+- a valid WordPress plugin skeleton
+- a block-ready folder structure
+- practical PHP and JS linting and validation
+- AI-ready workflows with prompts, reports, tasks, and agent personas
+- clear placeholder conventions throughout
+
+---
+
+## Who it is for
+
+LightSpeed developers building custom WordPress plugins — for client and commercial work.
+Also suitable for use by AI agents working within a structured, well-documented repo.
+
+---
+
+## What it includes
+
+| Area | Description |
+|---|---|
+| `{{PLUGIN_SLUG}}.php` | Main plugin bootstrap file |
+| `uninstall.php` | Safe plugin uninstall stub |
+| `inc/` | Optional PHP include files |
+| `src/` | Source files for block and plugin assets |
+| `blocks/` | Built or registered block asset directories |
+| `assets/` | Static CSS, JS, images, and icons |
+| `patterns/` | WordPress block patterns |
+| `templates/` | Optional block templates |
+| `languages/` | Translation files |
+| `docs/` | End-user documentation |
+| `.github/` | GitHub-native AI and workflow infrastructure |
+| `.agents/` | Portable agent skills and personas |
+
+---
+
+## Requirements
+
+- PHP 8.0+
+- WordPress 6.4+
+- Node.js 20+ (see `.nvmrc`)
+- Composer
+
+---
+
+## Quick start
+
+```bash
+# 1. Use this template via GitHub (recommended):
+# Click "Use this template" on the GitHub repo page,
+# or clone and replace placeholders manually:
+git clone https://github.com/{{GITHUB_ORG}}/{{REPO_NAME}}.git my-plugin
+cd my-plugin
+
+# 2. Replace all {{PLACEHOLDER}} tokens (see Customise placeholders below)
+# Rename {{PLUGIN_SLUG}}.php to match your actual plugin slug.
+
+# 3. Install Node dependencies
+npm install
+
+# 4. Install Composer dependencies
+composer install
+
+# 5. Validate the plugin scaffold
+npm run plugin:validate
+
+# 6. Lint your code
+npm run lint
+composer run phpcs
+```
+
+---
+
+## Customise placeholders
+
+Search and replace these tokens across the entire repo before starting work:
+
+| Placeholder | Example value |
+|---|---|
+| `{{PLUGIN_NAME}}` | `My Awesome Plugin` |
+| `{{PLUGIN_SLUG}}` | `my-awesome-plugin` |
+| `{{TEXT_DOMAIN}}` | `my-awesome-plugin` |
+| `{{PLUGIN_URI}}` | `https://example.com/plugins/my-awesome-plugin` |
+| `{{PLUGIN_DESCRIPTION}}` | `A useful WordPress plugin.` |
+| `{{AUTHOR_NAME}}` | `LightSpeed` |
+| `{{AUTHOR_URI}}` | `https://lightspeedwp.agency` |
+| `{{NAMESPACE}}` | `MY_AWESOME_PLUGIN` |
+| `{{PACKAGE_NAME}}` | `lightspeedwp/my-awesome-plugin` |
+| `{{REPO_NAME}}` | `my-awesome-plugin` |
+| `{{GITHUB_ORG}}` | `lightspeedwp` |
+
+Also rename `{{PLUGIN_SLUG}}.php` to match your actual plugin slug.
+
+---
+
+## Repo map
+
+```
+/
+├── {{PLUGIN_SLUG}}.php Main plugin bootstrap
+├── uninstall.php Plugin uninstall handler
+├── plugin-utils.mjs Plugin validation and utility CLI
+├── package.json Node tooling and scripts
+├── composer.json PHP tooling and standards
+├── AGENTS.md AI agent guidance (start here for AI)
+├── CLAUDE.md Claude-specific pointer
+├── CHANGELOG.md Version history
+├── docs/ End-user documentation
+├── inc/ PHP include files
+├── src/ Source files (blocks, CSS, JS)
+├── blocks/ Built/registered block assets
+├── assets/ Static assets (css, js, images, icons)
+├── patterns/ WordPress block patterns
+├── templates/ Block templates (optional)
+├── languages/ Translation files
+├── .github/ GitHub workflows, prompts, reports, tasks
+└── .agents/ Portable agent skills and personas
+```
+
+---
+
+## Available commands
+
+### Node
+
+| Command | Description |
+|---|---|
+| `npm run plugin:validate` | Validate plugin structure and headers |
+| `npm run schema:validate` | Validate block.json and JSON files |
+| `npm run security:scan` | Scan PHP files for risky patterns |
+| `npm run lint` | Run all JS, CSS, and JSON linters |
+| `npm run lint:js` | Lint JS source files |
+| `npm run lint:css` | Lint CSS source files |
+| `npm run lint:json` | Lint JSON files |
+| `npm run build` | Build block assets |
+| `npm run start` | Watch and build block assets |
+| `npm run i18n` | Generate translation POT file |
+
+### Composer
+
+| Command | Description |
+|---|---|
+| `composer run phpcs` | Check PHP coding standards |
+| `composer run phpcbf` | Auto-fix PHP coding standards |
+| `composer run phplint` | Check PHP syntax |
+
+---
+
+## What to edit first
+
+1. Replace all placeholders (see table above).
+2. Rename `{{PLUGIN_SLUG}}.php` to your plugin slug.
+3. Update `CHANGELOG.md` with your real start date.
+4. Update `docs/README.md` with your plugin's documentation structure.
+5. Update `CODEOWNERS` with real GitHub usernames.
+6. Add your PHP includes to `inc/` and load them from the main plugin file.
+7. Add blocks to `src/blocks/` and register them in the main plugin file.
+
+---
+
+## AI workflows
+
+| Folder | Purpose |
+|---|---|
+| `AGENTS.md` | Primary AI agent guidance for this repo |
+| `.github/copilot-instructions.md` | GitHub Copilot instructions |
+| `.github/instructions/` | File-type-specific Copilot guidance |
+| `.github/prompts/` | Reusable GitHub Copilot prompt files |
+| `.github/reports/` | Developer and AI-generated reports |
+| `.github/tasks/` | Task lists and AI work tracking |
+| `.agents/skills/` | Portable agent skills |
+| `.agents/agents/` | Agent persona definitions |
+
+---
+
+## Notes
+
+- This repo is **not** packaged specifically for WordPress.org submission.
+- `composer.lock` is not committed (this is a plugin, not a deployment artefact).
+- `package-lock.json` is committed to pin Node dependencies.
\ No newline at end of file
diff --git a/assets/css/.gitkeep b/assets/css/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/assets/icons/.gitkeep b/assets/icons/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/assets/js/.gitkeep b/assets/js/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/blocks/.gitkeep b/blocks/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..1d6e5c0
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,31 @@
+{
+ "name": "{{PACKAGE_NAME}}",
+ "type": "wordpress-plugin",
+ "license": "GPL-2.0-or-later",
+ "authors": [
+ {
+ "name": "{{AUTHOR_NAME}}",
+ "homepage": "{{AUTHOR_URI}}"
+ }
+ ],
+ "require": {
+ "php": ">=8.0"
+ },
+ "require-dev": {
+ "squizlabs/php_codesniffer": "^3.10",
+ "wp-coding-standards/wpcs": "^3.1",
+ "phpcompatibility/phpcompatibility-wp": "^2.1",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0"
+ },
+ "scripts": {
+ "phpcs": "phpcs --standard=WordPress .",
+ "phpcbf": "phpcbf --standard=WordPress .",
+ "phplint": "find . -name '*.php' -not -path './vendor/*' -not -path './node_modules/*' | xargs php -l"
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ },
+ "optimize-autoloader": true
+ }
+}
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..54313ba
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,36 @@
+# docs
+
+This folder contains **end-user documentation** for {{PLUGIN_NAME}}.
+
+---
+
+## What belongs here
+
+- Installation and setup guides
+- Editor and block usage guides
+- Client-facing usage notes
+- Screenshots and annotated walkthroughs
+- FAQs and troubleshooting notes
+
+## What does NOT belong here
+
+- Developer reports → `.github/reports/`
+- Task lists → `.github/tasks/`
+- AI prompts → `.github/prompts/`
+- Developer onboarding notes → `README.md` or `AGENTS.md`
+
+---
+
+## Suggested structure
+
+As the plugin grows, organise docs by audience or topic:
+
+```
+docs/
+├── README.md This file — overview of docs structure
+├── installation.md How to install and activate the plugin
+├── usage.md How to use the plugin's blocks and features
+└── faq.md Common questions and answers
+```
+
+Add screenshots and images to `docs/` as needed.
diff --git a/inc/.gitkeep b/inc/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/languages/.gitkeep b/languages/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c09d198
--- /dev/null
+++ b/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "{{PACKAGE_NAME}}",
+ "version": "0.1.0",
+ "description": "{{PLUGIN_DESCRIPTION}}",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "build": "wp-scripts build",
+ "start": "wp-scripts start",
+ "plugin:validate": "node plugin-utils.mjs validate-plugin",
+ "schema:validate": "node plugin-utils.mjs validate-schema",
+ "security:scan": "node plugin-utils.mjs security-scan",
+ "lint": "npm run lint:js && npm run lint:css && npm run lint:json",
+ "lint:js": "wp-scripts lint-js src",
+ "lint:css": "wp-scripts lint-style src",
+ "lint:json": "wp-scripts lint-md-docs . --ext=.json || true",
+ "i18n": "wp-scripts makepot",
+ "packages-update": "wp-scripts packages-update",
+ "help": "node plugin-utils.mjs help"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "devDependencies": {
+ "@wordpress/scripts": "^30.0.0"
+ }
+}
diff --git a/patterns/.gitkeep b/patterns/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/plugin-utils.mjs b/plugin-utils.mjs
new file mode 100644
index 0000000..bcfb94f
--- /dev/null
+++ b/plugin-utils.mjs
@@ -0,0 +1,433 @@
+#!/usr/bin/env node
+/**
+ * plugin-utils.mjs
+ *
+ * Plugin validation and utility CLI for {{PLUGIN_NAME}}.
+ * Run with: node plugin-utils.mjs
+ *
+ * Commands:
+ * validate-plugin Validate plugin structure and headers
+ * validate-schema Validate block.json and JSON schema files
+ * security-scan Scan PHP files for risky patterns
+ * scan-placeholders Check for unreplaced placeholder tokens
+ * help Show available commands
+ */
+
+import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
+import { join, relative } from 'path';
+
+// ─── Helpers ────────────────────────────────────────────────────────────────
+
+const ROOT = process.cwd();
+
+const colours = {
+ reset: '\x1b[0m',
+ red: '\x1b[31m',
+ yellow: '\x1b[33m',
+ green: '\x1b[32m',
+ cyan: '\x1b[36m',
+ bold: '\x1b[1m',
+};
+
+const log = {
+ info: ( msg ) => console.log( ` ${msg}` ),
+ ok: ( msg ) => console.log( `${colours.green} ✓${colours.reset} ${msg}` ),
+ warn: ( msg ) => console.log( `${colours.yellow} ⚠ ${msg}${colours.reset}` ),
+ error: ( msg ) => console.log( `${colours.red} ✗ ${msg}${colours.reset}` ),
+ heading: ( msg ) => console.log( `\n${colours.bold}${colours.cyan}${msg}${colours.reset}\n` ),
+};
+
+/**
+ * Recursively find files matching a predicate.
+ *
+ * @param {string} dir
+ * @param {Function} predicate
+ * @param {string[]} results
+ * @returns {string[]}
+ */
+function findFiles( dir, predicate, results = [] ) {
+ if ( ! existsSync( dir ) ) return results;
+
+ for ( const entry of readdirSync( dir ) ) {
+ const fullPath = join( dir, entry );
+
+ // Skip common large directories.
+ if ( [ 'node_modules', 'vendor', '.git', 'build', 'dist' ].includes( entry ) ) {
+ continue;
+ }
+
+ const stat = statSync( fullPath );
+ if ( stat.isDirectory() ) {
+ findFiles( fullPath, predicate, results );
+ } else if ( predicate( entry, fullPath ) ) {
+ results.push( fullPath );
+ }
+ }
+
+ return results;
+}
+
+/**
+ * Read a file as a string, return empty string on failure.
+ *
+ * @param {string} filePath
+ * @returns {string}
+ */
+function readFileSafe( filePath ) {
+ try {
+ return readFileSync( filePath, 'utf8' );
+ } catch {
+ return '';
+ }
+}
+
+// ─── Commands ────────────────────────────────────────────────────────────────
+
+/**
+ * validate-plugin
+ * Check that required plugin files exist and the main plugin header is valid.
+ */
+function validatePlugin() {
+ log.heading( 'Validating plugin structure…' );
+
+ let errors = 0;
+ let warnings = 0;
+
+ // Required files.
+ const required = [
+ 'uninstall.php',
+ 'README.md',
+ 'CHANGELOG.md',
+ 'package.json',
+ 'composer.json',
+ 'plugin-utils.mjs',
+ ];
+
+ for ( const file of required ) {
+ if ( existsSync( join( ROOT, file ) ) ) {
+ log.ok( `${file} exists` );
+ } else {
+ log.error( `Missing required file: ${file}` );
+ errors++;
+ }
+ }
+
+ // Find the main plugin file (a PHP file in root with a plugin header).
+ const phpFiles = readdirSync( ROOT ).filter(
+ ( f ) => f.endsWith( '.php' ) && f !== 'uninstall.php'
+ );
+
+ const mainPluginFile = phpFiles.find( ( file ) => {
+ const content = readFileSafe( join( ROOT, file ) );
+ return content.includes( 'Plugin Name:' );
+ } );
+
+ if ( mainPluginFile ) {
+ log.ok( `Main plugin file found: ${mainPluginFile}` );
+
+ const content = readFileSafe( join( ROOT, mainPluginFile ) );
+
+ // Check for ABSPATH protection.
+ if ( content.includes( "defined( 'ABSPATH' )" ) || content.includes( "defined('ABSPATH')" ) ) {
+ log.ok( 'Direct access protection (ABSPATH check) present' );
+ } else {
+ log.error( 'Missing ABSPATH direct access protection in main plugin file' );
+ errors++;
+ }
+
+ // Check for Text Domain header.
+ const textDomainMatch = content.match( /Text Domain:\s*(.+)/i );
+ if ( textDomainMatch ) {
+ const textDomain = textDomainMatch[ 1 ].trim();
+ log.ok( `Text Domain: ${textDomain}` );
+
+ // Warn if text domain still contains a placeholder.
+ if ( textDomain.includes( '{{' ) ) {
+ log.warn( 'Text Domain still contains a placeholder token — replace before use' );
+ warnings++;
+ }
+ } else {
+ log.warn( 'Text Domain header not found in main plugin file' );
+ warnings++;
+ }
+
+ // Check plugin name.
+ const pluginNameMatch = content.match( /Plugin Name:\s*(.+)/i );
+ if ( pluginNameMatch ) {
+ const pluginName = pluginNameMatch[ 1 ].trim();
+ if ( pluginName.includes( '{{' ) ) {
+ log.warn( `Plugin Name still contains a placeholder: ${pluginName}` );
+ warnings++;
+ } else {
+ log.ok( `Plugin Name: ${pluginName}` );
+ }
+ }
+ } else {
+ log.error( 'No main plugin file found in root (a PHP file with "Plugin Name:" header)' );
+ errors++;
+ }
+
+ // Check required folders.
+ const requiredDirs = [ 'inc', 'src', 'blocks', 'assets', 'docs', 'languages' ];
+ for ( const dir of requiredDirs ) {
+ if ( existsSync( join( ROOT, dir ) ) ) {
+ log.ok( `Directory exists: ${dir}/` );
+ } else {
+ log.warn( `Directory missing: ${dir}/ — create it when needed` );
+ warnings++;
+ }
+ }
+
+ // Summary.
+ console.log( '' );
+ if ( errors > 0 ) {
+ log.error( `Validation failed with ${errors} error(s) and ${warnings} warning(s).` );
+ process.exit( 1 );
+ } else if ( warnings > 0 ) {
+ log.warn( `Validation passed with ${warnings} warning(s). Review before release.` );
+ } else {
+ log.ok( 'Plugin structure looks good.' );
+ }
+}
+
+/**
+ * validate-schema
+ * Validate block.json files and other plugin JSON files.
+ */
+function validateSchema() {
+ log.heading( 'Validating JSON and block.json files…' );
+
+ const jsonFiles = findFiles( ROOT, ( name ) => name.endsWith( '.json' ) );
+
+ if ( jsonFiles.length === 0 ) {
+ log.info( 'No JSON files found.' );
+ return;
+ }
+
+ let errors = 0;
+
+ for ( const filePath of jsonFiles ) {
+ const rel = relative( ROOT, filePath );
+ const content = readFileSafe( filePath );
+
+ try {
+ const parsed = JSON.parse( content );
+
+ // For block.json files, check for key fields.
+ if ( filePath.endsWith( 'block.json' ) ) {
+ const missing = [];
+ if ( ! parsed.name ) missing.push( 'name' );
+ if ( ! parsed.title ) missing.push( 'title' );
+ if ( ! parsed.category ) missing.push( 'category' );
+ if ( ! parsed[ '$schema' ] ) missing.push( '$schema' );
+
+ if ( missing.length > 0 ) {
+ log.warn( `${rel}: block.json missing recommended fields: ${missing.join( ', ' )}` );
+ } else {
+ log.ok( `${rel}: valid block.json` );
+ }
+ } else {
+ log.ok( `${rel}: valid JSON` );
+ }
+ } catch ( err ) {
+ log.error( `${rel}: invalid JSON — ${err.message}` );
+ errors++;
+ }
+ }
+
+ console.log( '' );
+ if ( errors > 0 ) {
+ log.error( `Schema validation failed with ${errors} error(s).` );
+ process.exit( 1 );
+ } else {
+ log.ok( 'All JSON files are valid.' );
+ }
+}
+
+/**
+ * security-scan
+ * Scan PHP files for common risky patterns.
+ * This is a practical, lightweight scan — not a replacement for a proper SAST tool.
+ */
+function securityScan() {
+ log.heading( 'Scanning PHP files for risky patterns…' );
+
+ // Patterns that suggest risky code.
+ const riskyPatterns = [
+ {
+ label: 'Unescaped echo of $_GET/$_POST/$_REQUEST/$_COOKIE',
+ regex: /echo\s+\$_(GET|POST|REQUEST|COOKIE)\[/,
+ severity: 'error',
+ },
+ {
+ // Flag direct assignment of superglobals to a variable without an obvious
+ // sanitisation wrapper on the same line. Common safe wrappers are excluded
+ // to reduce false positives, but manual review is still recommended.
+ label: 'Possible unsanitised superglobal assignment — verify sanitisation',
+ regex: /=\s*\$_(GET|POST|REQUEST|COOKIE)\[/,
+ // Exclude lines that already call a known sanitisation function on the same line.
+ exclude: /sanitize_|esc_url_raw|absint|intval|wp_unslash/,
+ severity: 'warn',
+ note: 'Confirm input is sanitised, e.g. sanitize_text_field( wp_unslash( $_POST["field"] ) )',
+ },
+ {
+ label: 'eval() usage',
+ regex: /\beval\s*\(/,
+ severity: 'error',
+ },
+ {
+ label: 'base64_decode used — review for obfuscated payloads',
+ regex: /base64_decode\s*\(/,
+ severity: 'warn',
+ },
+ {
+ label: 'Potential SQL injection — unescaped $wpdb->query with variable',
+ regex: /\$wpdb->(query|get_results|get_row|get_var)\s*\(\s*["']?\s*\$/,
+ severity: 'warn',
+ note: 'Use $wpdb->prepare() or verify this is safe',
+ },
+ {
+ label: 'Direct file inclusion with variable',
+ regex: /(include|require)(_once)?\s*\(\s*\$/,
+ severity: 'warn',
+ note: 'Ensure path is validated and sanitised',
+ },
+ {
+ label: 'system/exec/shell_exec/passthru usage',
+ regex: /\b(system|exec|shell_exec|passthru)\s*\(/,
+ severity: 'error',
+ },
+ ];
+
+ const phpFiles = findFiles( ROOT, ( name ) => name.endsWith( '.php' ) );
+
+ if ( phpFiles.length === 0 ) {
+ log.info( 'No PHP files found.' );
+ return;
+ }
+
+ let totalIssues = 0;
+
+ for ( const filePath of phpFiles ) {
+ const rel = relative( ROOT, filePath );
+ const lines = readFileSafe( filePath ).split( '\n' );
+
+ for ( let i = 0; i < lines.length; i++ ) {
+ const line = lines[ i ];
+
+ for ( const pattern of riskyPatterns ) {
+ if ( pattern.regex.test( line ) ) {
+ // Skip if the line also matches an exclusion pattern (indicates safe usage).
+ if ( pattern.exclude && pattern.exclude.test( line ) ) {
+ continue;
+ }
+ const lineNum = i + 1;
+ if ( pattern.severity === 'error' ) {
+ log.error( `${rel}:${lineNum} — ${pattern.label}` );
+ } else {
+ log.warn( `${rel}:${lineNum} — ${pattern.label}` );
+ }
+ if ( pattern.note ) {
+ log.info( ` Note: ${pattern.note}` );
+ }
+ totalIssues++;
+ }
+ }
+ }
+ }
+
+ console.log( '' );
+ if ( totalIssues === 0 ) {
+ log.ok( `Scanned ${phpFiles.length} PHP file(s) — no obvious risky patterns found.` );
+ } else {
+ log.warn(
+ `Scanned ${phpFiles.length} PHP file(s) — ${totalIssues} potential issue(s) found. Review carefully.`
+ );
+ }
+}
+
+/**
+ * scan-placeholders
+ * Check for unreplaced {{PLACEHOLDER}} tokens.
+ */
+function scanPlaceholders() {
+ log.heading( 'Scanning for unreplaced placeholder tokens…' );
+
+ const scanExtensions = [ '.php', '.json', '.md', '.txt', '.yml', '.yaml', '.mjs', '.js' ];
+ const placeholderRegex = /\{\{[A-Z_]+\}\}/g;
+
+ const files = findFiles(
+ ROOT,
+ ( name ) => scanExtensions.some( ( ext ) => name.endsWith( ext ) )
+ );
+
+ let found = 0;
+
+ for ( const filePath of files ) {
+ const rel = relative( ROOT, filePath );
+ const content = readFileSafe( filePath );
+ const matches = content.match( placeholderRegex );
+
+ if ( matches ) {
+ const unique = [ ...new Set( matches ) ];
+ log.warn( `${rel}: ${unique.join( ', ' )}` );
+ found += unique.length;
+ }
+ }
+
+ console.log( '' );
+ if ( found === 0 ) {
+ log.ok( 'No unreplaced placeholder tokens found.' );
+ } else {
+ log.warn( `Found ${found} placeholder token(s) across scanned files. Replace before deploying.` );
+ }
+}
+
+/**
+ * help
+ * Display available commands.
+ */
+function showHelp() {
+ console.log( `
+${colours.bold}plugin-utils.mjs — {{PLUGIN_NAME}} validation and utility CLI${colours.reset}
+
+Usage:
+ node plugin-utils.mjs
+
+Commands:
+ ${colours.cyan}validate-plugin${colours.reset} Validate plugin structure and headers
+ ${colours.cyan}validate-schema${colours.reset} Validate block.json and JSON files
+ ${colours.cyan}security-scan${colours.reset} Scan PHP files for risky patterns
+ ${colours.cyan}scan-placeholders${colours.reset} Check for unreplaced {{PLACEHOLDER}} tokens
+ ${colours.cyan}help${colours.reset} Show this help message
+` );
+}
+
+// ─── Entrypoint ──────────────────────────────────────────────────────────────
+
+const command = process.argv[ 2 ];
+
+switch ( command ) {
+ case 'validate-plugin':
+ validatePlugin();
+ break;
+ case 'validate-schema':
+ validateSchema();
+ break;
+ case 'security-scan':
+ securityScan();
+ break;
+ case 'scan-placeholders':
+ scanPlaceholders();
+ break;
+ case 'help':
+ case '--help':
+ case '-h':
+ case undefined:
+ showHelp();
+ break;
+ default:
+ log.error( `Unknown command: ${command}` );
+ showHelp();
+ process.exit( 1 );
+}
diff --git a/readme.txt b/readme.txt
new file mode 100644
index 0000000..3a4a69e
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,26 @@
+=== {{PLUGIN_NAME}} ===
+Contributors: {{AUTHOR_NAME}}
+Tags: blocks, gutenberg, {{PLUGIN_SLUG}}
+Requires at least: 6.4
+Tested up to: 6.7
+Requires PHP: 8.0
+Stable tag: 0.1.0
+License: GPL-2.0-or-later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
+
+{{PLUGIN_DESCRIPTION}}
+
+== Description ==
+
+{{PLUGIN_DESCRIPTION}}
+
+== Installation ==
+
+1. Upload the plugin folder to `/wp-content/plugins/`.
+2. Activate the plugin through the Plugins screen in WordPress.
+3. See `docs/` for further usage notes.
+
+== Changelog ==
+
+= 0.1.0 =
+* Initial release.
diff --git a/src/.gitkeep b/src/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/blocks/.gitkeep b/src/blocks/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/css/.gitkeep b/src/css/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/js/.gitkeep b/src/js/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/templates/.gitkeep b/templates/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/uninstall.php b/uninstall.php
new file mode 100644
index 0000000..9a81097
--- /dev/null
+++ b/uninstall.php
@@ -0,0 +1,30 @@
+query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '{{PLUGIN_SLUG}}_%'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+
+// Example: Remove a custom database table.
+// global $wpdb;
+// $table_name = $wpdb->prefix . '{{PLUGIN_SLUG}}_data';
+// $wpdb->query( "DROP TABLE IF EXISTS {$table_name}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
diff --git a/{{PLUGIN_SLUG}}.php b/{{PLUGIN_SLUG}}.php
new file mode 100644
index 0000000..b0a13f7
--- /dev/null
+++ b/{{PLUGIN_SLUG}}.php
@@ -0,0 +1,49 @@
+