# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this repo is

A **Claude Code plugin**, not an application. Almost everything here is markdown that instructs
Claude at runtime — commands, agents and skills. There is no build step, no package manager, no
runtime for the plugin itself. The only executable code shipped is:

- `bin/*.sh` — env setup / ffmpeg wrappers, invoked by commands
- `skills/wp-polylang/scripts/*.php` — run inside a real WordPress via `wp eval-file`
- `starter-theme/**` — PHP copied into user projects (never executed here)

Editing a "feature" therefore usually means editing prose contracts, and the tests assert on
those contracts (grep for required tokens in the markdown), not on program behavior.

## Testing

Each check is a standalone bash script that prints `PASS` or exits non-zero. No runner, no framework.

```bash
bash tests/checks/wp-yolo-gate.sh                    # one check
for f in tests/checks/*.sh; do bash "$f"; done       # all checks
```

`tests/checks/wp-polylang-live.sh` is the only test that touches a real site; it exits 0 with
`SKIP` unless `PLL_TEST_SITE` points at a WordPress root with Polylang active
(`PLL_TEST_SRC`/`PLL_TEST_DST`/`PLL_TEST_THIRD` override the languages; the third language is
created and deleted by the suite and is guarded by a pre-existence probe).

There is no CI. Run the checks yourself before claiming a change works.

## Architecture

Four layers, strictly ordered:

```
commands/*.md   user-facing slash commands — orchestration, argument parsing, step-by-step flow
   ↓ dispatches
agents/*.md     specialized subagents (wp-template, wp-css, wp-acf, wp-audit-*, wp-normalize…)
   ↓ reads
skills/*/SKILL.md   knowledge libraries — methodology, never actions. user-invocable: false
   ↓ produces
starter-theme/ + templates/   PHP theme scaffolds and env/plugin-profile configs copied per project
```

Key consequences when changing things:

- A command must not reimplement a builder. `/wp-yolo` is the model: it normalizes the demo once,
  then drives the existing commands/agents in dependency order.
- Every agent's first mandatory step is to read the **project's** `.claude/CLAUDE.md` (generated by
  `/wp-init` in the user's WordPress project — a different file from this one) to pick up the
  function prefix, languages and theme slug. `prefix_` in agent/skill docs is a placeholder.
- Plugin-relative paths in commands are always `${CLAUDE_PLUGIN_ROOT}/…`, never relative.
- The `.wp-create.json` manifest in a user project is the shared source of truth for the WP-CLI
  wrapper, environment type and language config — commands read it instead of re-asking.

### Two independent i18n systems — do not mix them

| | `wp-bilingual` skill | `wp-polylang` skill |
|---|---|---|
| Model | one page, ACF field `_<lang>` suffixes (`hero_title_es`) | one post per language, joined by translation groups |
| Helpers | `prefix_get_field()`, `prefix_t()`, `prefix_e()` resolve the suffix transparently | `pll_*` API via `wp eval-file` scripts |
| Used by | `/wp-init` when `i18n strategy: suffix` (the default), `/wp-section`, `/wp-seed` | `/wp-init` when `i18n strategy: polylang`, and `/wp-polylang` to retrofit an existing site |

**Which one a project uses is a recorded decision, not a guess.** `/wp-init`
asks (Step 0.7) and writes the answer as `i18n strategy` into the project's
`.claude/CLAUDE.md`. Every downstream command and agent branches on that line —
`/wp-seed`, `/wp-header`, `wp-acf`, `/wp-yolo` — so read it before assuming the
suffix model. When the line is absent, the project predates the choice and is
`suffix`.

The seam is a single file: `inc/i18n.php`. Templates call `prefix_get_field()`
and friends and never `get_field()` directly, so swapping that file switches the
whole theme's model with no template changes. The Polylang variants live in
`starter-theme/_i18n-variants/<template>.php` and are copied over `inc/i18n.php`
by `/wp-init`. They are kept OUT of the theme directories on purpose: a starter
carrying two definitions of the same function fatals the moment anything globs
`inc/*.php`, and `tests/checks/tailwind-starter.sh` refuses that state. The two
starters have different contracts (nine helpers vs three), so each variant
mirrors its own, and `tests/checks/wp-polylang.sh` asserts the pairing per
starter.

One deliberate crossover: under Polylang, ACF **options-page** fields keep their
`_<lang>` suffixes. Options are global — one set of values per site, not per
language — so Polylang's one-post-per-language model does not reach them and the
free plugin does not translate them. `wp-acf` therefore drops `_<lang>`
duplicates everywhere except the settings group.

The Polylang path is real code with real failure modes: translations join through the
`post_translations` taxonomy, so anything that writes translations must go through
`pll_save_post_translations` / `pll_save_term_translations`. `pll-lib.php` bridges `wp eval-file`'s
local `$args` into `$GLOBALS` — the scripts do not work without that require. PHP 7.4 floor:
no `match`, no union types.

## Authoring conventions

**Command** (`commands/<name>.md`) — frontmatter with `description`, `allowed-tools`,
`argument-hint`. Add `Agent` to `allowed-tools` only if it dispatches subagents.

**Agent** (`agents/<name>.md`) — frontmatter `name`, `description`, `tools` (order:
`Read, Write, Edit, Grep, Glob, Bash`). Must open with the "First Action (MANDATORY)" block.

**Skill** (`skills/<name>/SKILL.md`) — frontmatter `name`, `description`, `user-invocable: false`.
Skills inform; they never act.

**Starter theme edits** — use the placeholder tokens (`__starter__`, `__STARTER__`,
`__STARTER_NAME__`), replaced by `/wp-init`.

**New behavior needs a check.** The house style is a grep-for-required-tokens script under
`tests/checks/` that fails if the contract wording disappears from the command/skill/agent.

## Generated-theme conventions (what the agents must emit)

- Fields: `<section>_<element>` / repeaters `<section>_<plural>` / keys `field_<section>_<element>`,
  `group_<section>`; bilingual duplicates append `_<lang>`.
- Templates: always `prefix_get_field()` (never raw `get_field()`), `?:` fallbacks, everything
  escaped (`esc_html`, `esc_url`, `esc_attr`).
- CSS: custom properties for all tokens, BEM, mobile-first `min-width` queries, section delimiters
  `/* ====== Section: Hero ====== */`. In transcription mode (`--transcribe`, `/wp-yolo`) agents copy
  the demo's exact declared values instead of re-authoring them, and scope every selector under a
  unique BEM block so parallel section builds cannot collide.
- Prefer WP-CLI over generating PHP (`wp-cli-patterns` skill).

## Release chores

Version lives in `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` (twice) and the
README badge — bump all of them together. Add an `[Unreleased]` entry to `CHANGELOG.md`; update
`README.md` when adding a command.

## Known ceilings

These are deliberate, documented limits — not bugs to "fix" on sight:

- **Media is not translated.** The Polylang importer copies an image or file id to the counterpart
  as-is rather than swapping it for that attachment's own translation. Mapping media is a separate
  decision; `pll-import.php` and the live suite both state the ceiling.
- **ACF reference re-pointing is one level deep.** `link`, `page_link`, `post_object` and
  `relationship` are re-pointed only as top-level fields, matching `pllx_acf_walk()`'s own ceiling.
  The same field nested inside a repeater or flexible-content row keeps pointing at the source.
- **Parent structure mirrors the source.** Both parent fixup passes rewrite the counterpart's parent
  on every run, so a post or term an editor deliberately re-parented in the target language is put
  back. Only the ACF reference pass records ownership; parents do not.
- **`/wp-init`'s Polylang path has never been run end-to-end against a fresh project.** Its scripts
  are covered by `tests/checks/wp-polylang-live.sh` against a real site, but the command's own
  branching is prose, verified only by the grep checks in `tests/checks/wp-polylang.sh` and
  `wp-init-templates.sh`.
