@linchpinagency/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +337 -0
  2. package/bin/install.mjs +231 -0
  3. package/package.json +44 -0
  4. package/skills/browser-automation/SKILL.md +93 -0
  5. package/skills/commit-and-release/SKILL.md +135 -0
  6. package/skills/dependency-updates/SKILL.md +102 -0
  7. package/skills/design-previews/SKILL.md +118 -0
  8. package/skills/engagement-types/SKILL.md +108 -0
  9. package/skills/investigate/SKILL.md +95 -0
  10. package/skills/project-context/SKILL.md +89 -0
  11. package/skills/quality-gates/SKILL.md +94 -0
  12. package/skills/quality-gates/references/toolchain.md +104 -0
  13. package/skills/safety-hooks/SKILL.md +121 -0
  14. package/skills/safety-hooks/scripts/check-destructive.sh +80 -0
  15. package/skills/safety-hooks/scripts/check-edit-boundary.sh +65 -0
  16. package/skills/support-triage/SKILL.md +103 -0
  17. package/skills/task-tracking/SKILL.md +243 -0
  18. package/skills/web-qa/SKILL.md +108 -0
  19. package/skills/web-qa/references/qa-checklist.md +98 -0
  20. package/skills/wordpress-blocks/SKILL.md +110 -0
  21. package/skills/wordpress-blocks/references/block-grammar.md +94 -0
  22. package/skills/wordpress-blocks/references/core-blocks.md +123 -0
  23. package/skills/wordpress-blocks/references/patterns-and-parts.md +70 -0
  24. package/skills/wordpress-blocks/references/recipes/faq.md +56 -0
  25. package/skills/wordpress-blocks/references/recipes/hero.md +74 -0
  26. package/skills/wordpress-blocks/references/recipes/pricing-table.md +77 -0
  27. package/skills/wordpress-blocks/references/tool-contract.md +167 -0
  28. package/skills/wordpress-blocks/references/validation.md +38 -0
  29. package/skills/wp-audit/SKILL.md +115 -0
  30. package/skills/wp-block-conventions/SKILL.md +134 -0
  31. package/skills/wp-block-conventions/references/block-anatomy.md +175 -0
  32. package/skills/wp-implementation-choice/SKILL.md +88 -0
  33. package/skills/wp-local-setup/SKILL.md +262 -0
  34. package/skills/wp-pressable/SKILL.md +172 -0
  35. package/skills/wp-studio-cli/SKILL.md +165 -0
  36. package/skills/write-a-linchpin-skill/SKILL.md +195 -0
  37. package/skills/write-a-linchpin-skill/references/template.md +83 -0
  38. package/upstream.json +20 -0
@@ -0,0 +1,123 @@
1
+ # Core block cheat sheet
2
+
3
+ Copy-paste-correct markup for the core blocks you'll compose with most. Shapes drift slightly
4
+ by WordPress version — always `validate_blocks` the result (see `validation.md`). Core blocks
5
+ omit the `core/` namespace.
6
+
7
+ ## Text
8
+
9
+ **Paragraph**
10
+ ```html
11
+ <!-- wp:paragraph -->
12
+ <p>Body copy goes here.</p>
13
+ <!-- /wp:paragraph -->
14
+ ```
15
+
16
+ **Heading** (default level is `h2`; set `{"level":N}` AND change the tag together)
17
+ ```html
18
+ <!-- wp:heading -->
19
+ <h2 class="wp-block-heading">Section title</h2>
20
+ <!-- /wp:heading -->
21
+
22
+ <!-- wp:heading {"level":3} -->
23
+ <h3 class="wp-block-heading">Subsection</h3>
24
+ <!-- /wp:heading -->
25
+ ```
26
+
27
+ **List** (each item is its own `wp:list-item`)
28
+ ```html
29
+ <!-- wp:list -->
30
+ <ul class="wp-block-list"><!-- wp:list-item -->
31
+ <li>First</li>
32
+ <!-- /wp:list-item -->
33
+
34
+ <!-- wp:list-item -->
35
+ <li>Second</li>
36
+ <!-- /wp:list-item --></ul>
37
+ <!-- /wp:list -->
38
+ ```
39
+
40
+ ## Layout containers
41
+
42
+ **Group** (constrained = respects theme content width; also `"flex"`, `"flow"`)
43
+ ```html
44
+ <!-- wp:group {"layout":{"type":"constrained"}} -->
45
+ <div class="wp-block-group">
46
+ <!-- child blocks -->
47
+ </div>
48
+ <!-- /wp:group -->
49
+ ```
50
+
51
+ **Columns** (note nesting: `wp:columns` → `wp:column` → children)
52
+ ```html
53
+ <!-- wp:columns -->
54
+ <div class="wp-block-columns"><!-- wp:column -->
55
+ <div class="wp-block-column">
56
+ <!-- column 1 children -->
57
+ </div>
58
+ <!-- /wp:column -->
59
+
60
+ <!-- wp:column -->
61
+ <div class="wp-block-column">
62
+ <!-- column 2 children -->
63
+ </div>
64
+ <!-- /wp:column --></div>
65
+ <!-- /wp:columns -->
66
+ ```
67
+ Set widths with `{"width":"33.33%"}` on each `wp:column`. Stacking on mobile is on by default;
68
+ `{"isStackedOnMobile":false}` to disable.
69
+
70
+ **Spacer** (has inner HTML — NOT self-closing)
71
+ ```html
72
+ <!-- wp:spacer {"height":"40px"} -->
73
+ <div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>
74
+ <!-- /wp:spacer -->
75
+ ```
76
+
77
+ ## Media & actions
78
+
79
+ **Image** (`id` ties to a real attachment; omit it for external/placeholder `src`)
80
+ ```html
81
+ <!-- wp:image {"id":123,"sizeSlug":"large","linkDestination":"none"} -->
82
+ <figure class="wp-block-image size-large"><img src="https://example.com/x.jpg" alt="" class="wp-image-123"/></figure>
83
+ <!-- /wp:image -->
84
+ ```
85
+
86
+ **Buttons** (always wrap a `wp:button` inside `wp:buttons`)
87
+ ```html
88
+ <!-- wp:buttons -->
89
+ <div class="wp-block-buttons"><!-- wp:button -->
90
+ <div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="/contact">Get started</a></div>
91
+ <!-- /wp:button --></div>
92
+ <!-- /wp:buttons -->
93
+ ```
94
+
95
+ **Cover** (background image/overlay with inner content; `dimRatio` = overlay opacity)
96
+ ```html
97
+ <!-- wp:cover {"url":"https://example.com/bg.jpg","dimRatio":50,"minHeight":480} -->
98
+ <div class="wp-block-cover" style="min-height:480px"><span aria-hidden="true" class="wp-block-cover__background has-background-dim"></span><img class="wp-block-cover__image-background" src="https://example.com/bg.jpg" alt=""/><div class="wp-block-cover__inner-container">
99
+ <!-- inner blocks: heading, paragraph, buttons -->
100
+ </div></div>
101
+ <!-- /wp:cover -->
102
+ ```
103
+
104
+ ## Using design tokens
105
+
106
+ Prefer the site's theme.json slugs over hardcoded values (pull them from `get_theme_tokens()`):
107
+
108
+ ```html
109
+ <!-- wp:heading {"textColor":"primary","fontSize":"x-large"} -->
110
+ <h2 class="wp-block-heading has-primary-color has-text-color has-x-large-font-size">Title</h2>
111
+ <!-- /wp:heading -->
112
+ ```
113
+ The JSON attribute (`"textColor":"primary"`) and the generated classes
114
+ (`has-primary-color has-text-color`) must agree — this is why validation matters.
115
+
116
+ ## Reuse references (not composition)
117
+
118
+ These insert existing assets — see `patterns-and-parts.md`:
119
+ ```html
120
+ <!-- wp:block {"ref":451} /--> ← synced pattern (wp_block #451)
121
+ <!-- wp:pattern {"slug":"linchpin/pricing-3col"} /--> ← registered pattern reference
122
+ <!-- wp:template-part {"slug":"footer","theme":"linchpin"} /-->
123
+ ```
@@ -0,0 +1,70 @@
1
+ # Patterns and template parts — what to reuse, and how
2
+
3
+ The site already holds reusable design assets. Reusing them beats hand-composing core blocks:
4
+ it matches the designer's intent, stays on-brand, and (for synced patterns) keeps content
5
+ updatable in one place. Three distinct things get confused — here's the contract for each.
6
+
7
+ ## 1. Synced patterns (`wp_block` custom post type)
8
+
9
+ The successor to "reusable blocks." A saved block stored once and **linked** wherever it's
10
+ used — edit it in one place, every instance updates.
11
+
12
+ - **Find:** `search_patterns(query)` returns synced patterns with a numeric `ref` (the
13
+ `wp_block` post ID).
14
+ - **Emit:** a void reference block — do **not** inline its markup:
15
+ ```html
16
+ <!-- wp:block {"ref":451} /-->
17
+ ```
18
+ - **Edit one instance only:** the user must *detach* it (turns it into normal blocks). If
19
+ they ask to "change just this one," fetch the expanded markup via `get_pattern(ref:451)`,
20
+ insert that instead of the reference, and tell them it's now detached.
21
+ - **Best for:** repeated, centrally-managed content — a standard CTA band, a contact block,
22
+ a promo the client updates site-wide.
23
+
24
+ ## 2. Registered patterns (theme- or plugin-provided)
25
+
26
+ Reusable **layout templates** registered in code (theme `patterns/*.php`, a plugin, or the
27
+ Pattern Directory). Inserting one drops a **one-time copy** of its blocks — no live link.
28
+
29
+ - **Find:** `search_patterns(query)` returns them with a `slug` and `type:"registered"`.
30
+ - **Emit:** fetch and inline the expanded blocks, adapting the copy to the request:
31
+ ```
32
+ const { block_markup } = get_pattern(slug: "linchpin/pricing-3col")
33
+ // insert block_markup, then swap in the user's plan names/prices
34
+ ```
35
+ (Some hosts also accept the overlay form `<!-- wp:pattern {"slug":"…"} /-->`, but inlining
36
+ the expanded markup is the portable, editable choice.)
37
+ - **Best for:** starting a section from a designed layout the user will then customize.
38
+
39
+ ## 3. Template parts (`wp_template_part` — block themes / FSE)
40
+
41
+ Header, footer, and reusable section parts of a **block theme**. These belong to *templates*,
42
+ not to a page/post body.
43
+
44
+ - **Find:** `list_template_parts()` → `{slug, title, area, theme}`.
45
+ - **Emit (templates only):**
46
+ ```html
47
+ <!-- wp:template-part {"slug":"footer","theme":"linchpin","tagName":"footer"} /-->
48
+ ```
49
+ - **Do not** insert a template part into post/page content. If the user wants a footer-like
50
+ block *inside* a page, use a synced pattern or compose blocks instead.
51
+
52
+ ## How this maps to the pattern-first procedure
53
+
54
+ ```
55
+ request → search_patterns(keywords)
56
+ ├─ synced match? → <!-- wp:block {"ref":ID} /-->
57
+ ├─ registered match? → get_pattern(slug) → insert expanded markup, adapt copy
58
+ ├─ template part? → only if editing a template → wp:template-part
59
+ └─ no match → compose core blocks (recipes/ + core-blocks.md) using get_theme_tokens()
60
+ → validate_blocks → insert
61
+ ```
62
+
63
+ Prefer a **good** match over a forced one. A weak pattern match that needs heavy rewriting is
64
+ worse than a clean core-block composition. Use judgment: does the matched asset actually serve
65
+ what the user asked for, or just share a keyword?
66
+
67
+ Upstream concepts: synced patterns
68
+ <https://wordpress.org/documentation/article/reusable-blocks/>,
69
+ theme patterns <https://developer.wordpress.org/themes/patterns/>,
70
+ template parts <https://developer.wordpress.org/themes/block-themes/template-parts/>.
@@ -0,0 +1,56 @@
1
+ # Recipe: FAQ section (fallback)
2
+
3
+ Use after `search_patterns("faq questions accordion")` finds no good match.
4
+
5
+ ## Preferred — Details block (native accordion, no plugin)
6
+
7
+ Core `wp:details` renders an expand/collapse `<details>` natively. Each Q is its own block.
8
+
9
+ ```html
10
+ <!-- wp:group {"layout":{"type":"constrained"}} -->
11
+ <div class="wp-block-group">
12
+ <!-- wp:heading -->
13
+ <h2 class="wp-block-heading">Frequently asked questions</h2>
14
+ <!-- /wp:heading -->
15
+
16
+ <!-- wp:details -->
17
+ <details class="wp-block-details"><summary>How does billing work?</summary>
18
+ <!-- wp:paragraph -->
19
+ <p>You're billed monthly and can cancel anytime.</p>
20
+ <!-- /wp:paragraph -->
21
+ </details>
22
+ <!-- /wp:details -->
23
+
24
+ <!-- wp:details -->
25
+ <details class="wp-block-details"><summary>Do you offer support?</summary>
26
+ <!-- wp:paragraph -->
27
+ <p>Yes — email support on every plan.</p>
28
+ <!-- /wp:paragraph -->
29
+ </details>
30
+ <!-- /wp:details -->
31
+ </div>
32
+ <!-- /wp:group -->
33
+ ```
34
+
35
+ Set `{"showContent":true}` on a `wp:details` to render it open by default.
36
+
37
+ ## Simpler — heading + paragraph pairs (no interactivity)
38
+
39
+ If `wp:details` isn't registered on the site (`list_registered_blocks`), or the user wants a
40
+ flat list, alternate headings and paragraphs:
41
+
42
+ ```html
43
+ <!-- wp:heading {"level":3} -->
44
+ <h3 class="wp-block-heading">How does billing work?</h3>
45
+ <!-- /wp:heading -->
46
+
47
+ <!-- wp:paragraph -->
48
+ <p>You're billed monthly and can cancel anytime.</p>
49
+ <!-- /wp:paragraph -->
50
+ ```
51
+
52
+ ## Adapt
53
+
54
+ - Generate one `wp:details` (or heading/paragraph pair) per Q&A from the user's content.
55
+ - Don't reach for a third-party accordion block unless the user names one and
56
+ `list_registered_blocks` confirms it's installed — `wp:details` covers most needs.
@@ -0,0 +1,74 @@
1
+ # Recipe: hero / header section (fallback)
2
+
3
+ Use after `search_patterns("hero banner header")` finds no good match. Two variants.
4
+
5
+ ## Variant A — Cover (background image + overlay)
6
+
7
+ Best when the user wants an image or color behind the headline. `dimRatio` is overlay
8
+ strength (0–100). Inner blocks: heading, paragraph, buttons.
9
+
10
+ ```html
11
+ <!-- wp:cover {"url":"https://example.com/hero.jpg","dimRatio":50,"minHeight":520,"align":"full"} -->
12
+ <div class="wp-block-cover alignfull" style="min-height:520px"><span aria-hidden="true" class="wp-block-cover__background has-background-dim"></span><img class="wp-block-cover__image-background" src="https://example.com/hero.jpg" alt=""/><div class="wp-block-cover__inner-container">
13
+ <!-- wp:heading {"textAlign":"center","level":1,"fontSize":"xx-large"} -->
14
+ <h1 class="wp-block-heading has-text-align-center has-xx-large-font-size">Build faster with Linchpin</h1>
15
+ <!-- /wp:heading -->
16
+
17
+ <!-- wp:paragraph {"align":"center"} -->
18
+ <p class="has-text-align-center">A short, punchy subhead that explains the value in one line.</p>
19
+ <!-- /wp:paragraph -->
20
+
21
+ <!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
22
+ <div class="wp-block-buttons"><!-- wp:button -->
23
+ <div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="/contact">Get started</a></div>
24
+ <!-- /wp:button --></div>
25
+ <!-- /wp:buttons -->
26
+ </div></div>
27
+ <!-- /wp:cover -->
28
+ ```
29
+
30
+ ## Variant B — Group + columns (text left, image right, no overlay)
31
+
32
+ Best for a clean split layout.
33
+
34
+ ```html
35
+ <!-- wp:group {"align":"wide","layout":{"type":"constrained"}} -->
36
+ <div class="wp-block-group alignwide">
37
+ <!-- wp:columns {"verticalAlignment":"center"} -->
38
+ <div class="wp-block-columns are-vertically-aligned-center"><!-- wp:column {"verticalAlignment":"center"} -->
39
+ <div class="wp-block-column is-vertically-aligned-center">
40
+ <!-- wp:heading {"level":1,"fontSize":"xx-large"} -->
41
+ <h1 class="wp-block-heading has-xx-large-font-size">A headline that sells</h1>
42
+ <!-- /wp:heading -->
43
+
44
+ <!-- wp:paragraph -->
45
+ <p>One or two sentences of supporting copy.</p>
46
+ <!-- /wp:paragraph -->
47
+
48
+ <!-- wp:buttons -->
49
+ <div class="wp-block-buttons"><!-- wp:button -->
50
+ <div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="/contact">Get started</a></div>
51
+ <!-- /wp:button --></div>
52
+ <!-- /wp:buttons -->
53
+ </div>
54
+ <!-- /wp:column -->
55
+
56
+ <!-- wp:column {"verticalAlignment":"center"} -->
57
+ <div class="wp-block-column is-vertically-aligned-center">
58
+ <!-- wp:image {"sizeSlug":"large"} -->
59
+ <figure class="wp-block-image size-large"><img src="https://example.com/hero.jpg" alt=""/></figure>
60
+ <!-- /wp:image -->
61
+ </div>
62
+ <!-- /wp:column --></div>
63
+ <!-- /wp:columns -->
64
+ </div>
65
+ <!-- /wp:group -->
66
+ ```
67
+
68
+ ## Adapt
69
+
70
+ - Use `level:1` for the headline only if this is the page's main hero; otherwise `level:2`.
71
+ - Swap colors/sizes for `get_theme_tokens()` slugs. For a solid-color background instead of an
72
+ image, drop the `url`/`img` and add `{"customOverlayColor":"…"}` or a `backgroundColor` token.
73
+ - `align:"full"` / `align:"wide"` only render edge-to-edge if the theme supports those widths
74
+ (`get_theme_tokens().layout`).
@@ -0,0 +1,77 @@
1
+ # Recipe: pricing table (fallback)
2
+
3
+ Use this **only after** `search_patterns("pricing plans tiers")` finds no good synced/registered
4
+ pattern. This is a three-tier table composed from core blocks. Pull color/font slugs from
5
+ `get_theme_tokens()` and swap them in; `validate_blocks` the result.
6
+
7
+ ## Structure
8
+
9
+ A constrained `group` (section heading) followed by a `columns` block — one `column` per plan,
10
+ each a bordered `group` holding: plan name (`heading`), price (`heading`), feature `list`, and a
11
+ `buttons` CTA.
12
+
13
+ ```html
14
+ <!-- wp:group {"layout":{"type":"constrained"}} -->
15
+ <div class="wp-block-group">
16
+ <!-- wp:heading {"textAlign":"center"} -->
17
+ <h2 class="wp-block-heading has-text-align-center">Simple, transparent pricing</h2>
18
+ <!-- /wp:heading -->
19
+
20
+ <!-- wp:columns {"align":"wide"} -->
21
+ <div class="wp-block-columns alignwide"><!-- wp:column -->
22
+ <div class="wp-block-column">
23
+ <!-- wp:group {"style":{"border":{"width":"1px","radius":"8px"},"spacing":{"padding":{"top":"2rem","right":"2rem","bottom":"2rem","left":"2rem"}}},"layout":{"type":"constrained"}} -->
24
+ <div class="wp-block-group" style="border-width:1px;border-radius:8px;padding-top:2rem;padding-right:2rem;padding-bottom:2rem;padding-left:2rem">
25
+ <!-- wp:heading {"level":3,"textAlign":"center"} -->
26
+ <h3 class="wp-block-heading has-text-align-center">Starter</h3>
27
+ <!-- /wp:heading -->
28
+
29
+ <!-- wp:heading {"textAlign":"center","fontSize":"x-large"} -->
30
+ <h2 class="wp-block-heading has-text-align-center has-x-large-font-size">$19<span>/mo</span></h2>
31
+ <!-- /wp:heading -->
32
+
33
+ <!-- wp:list -->
34
+ <ul class="wp-block-list"><!-- wp:list-item -->
35
+ <li>1 site</li>
36
+ <!-- /wp:list-item -->
37
+
38
+ <!-- wp:list-item -->
39
+ <li>Community support</li>
40
+ <!-- /wp:list-item --></ul>
41
+ <!-- /wp:list -->
42
+
43
+ <!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
44
+ <div class="wp-block-buttons"><!-- wp:button -->
45
+ <div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="#">Choose Starter</a></div>
46
+ <!-- /wp:button --></div>
47
+ <!-- /wp:buttons -->
48
+ </div>
49
+ <!-- /wp:group -->
50
+ </div>
51
+ <!-- /wp:column -->
52
+
53
+ <!-- wp:column -->
54
+ <div class="wp-block-column">
55
+ <!-- duplicate the bordered group; vary name/price/features (e.g. "Pro", "$49/mo") -->
56
+ </div>
57
+ <!-- /wp:column -->
58
+
59
+ <!-- wp:column -->
60
+ <div class="wp-block-column">
61
+ <!-- third plan (e.g. "Business", "$99/mo") -->
62
+ </div>
63
+ <!-- /wp:column --></div>
64
+ <!-- /wp:columns -->
65
+ </div>
66
+ <!-- /wp:group -->
67
+ ```
68
+
69
+ ## Adapt
70
+
71
+ - Fill real plan names, prices, and features from the user's request.
72
+ - Highlight a "most popular" tier with a token background: add
73
+ `{"backgroundColor":"primary","textColor":"white"}` to that column's inner `group` (and the
74
+ matching `has-…` classes — let `validate_blocks` reconcile them).
75
+ - 2 plans → two columns; 4 → consider two rows of `columns` or narrower columns.
76
+ - Replace inline border/padding `style` with theme spacing/border tokens when
77
+ `get_theme_tokens()` exposes them.
@@ -0,0 +1,167 @@
1
+ # Tool contract — the live site interface
2
+
3
+ The static knowledge in this skill is generic. The **dynamic** half — *this* site's synced
4
+ patterns, registered patterns, template parts, registered blocks, and theme tokens — lives in
5
+ WordPress and changes per site and over time. It can't be a markdown file; the host (the Mantle
6
+ plugin) must expose it as **tools** the model calls at request time.
7
+
8
+ Design goals:
9
+ - **Index, then fetch.** `search_patterns` returns lightweight rows (title/slug/categories/
10
+ excerpt) so the model can choose cheaply; `get_pattern` pulls full markup only for the one
11
+ it picks. Never dump every pattern's full markup into the prompt.
12
+ - **Function-calling friendly.** Schemas below are JSON Schema, usable directly as OpenAI
13
+ `tools[].function.parameters`.
14
+
15
+ Implementation notes (host side): synced patterns come from the `wp_block` CPT; registered
16
+ patterns from `WP_Block_Patterns_Registry`; registered blocks from `WP_Block_Type_Registry`;
17
+ template parts from the `wp_template_part` source; tokens from the active theme's `theme.json`
18
+ (`wp_get_global_settings()`).
19
+
20
+ ---
21
+
22
+ ## `search_patterns`
23
+
24
+ > Search the site's reusable assets (synced + registered patterns and template parts). Call
25
+ > this FIRST for any layout/section request, before composing core blocks.
26
+
27
+ ```json
28
+ {
29
+ "name": "search_patterns",
30
+ "description": "Search the site's synced patterns, registered patterns, and template parts by keyword. Returns lightweight matches; call get_pattern to retrieve full markup.",
31
+ "parameters": {
32
+ "type": "object",
33
+ "properties": {
34
+ "query": { "type": "string", "description": "Keywords from the user's intent, e.g. 'pricing plans tiers'." },
35
+ "types": {
36
+ "type": "array",
37
+ "items": { "type": "string", "enum": ["synced", "registered", "template_part"] },
38
+ "description": "Optional filter. Default: all three."
39
+ },
40
+ "limit": { "type": "integer", "minimum": 1, "maximum": 25, "default": 8 }
41
+ },
42
+ "required": ["query"]
43
+ }
44
+ }
45
+ ```
46
+
47
+ **Returns:** `{ "matches": Match[] }`
48
+ ```json
49
+ {
50
+ "matches": [
51
+ {
52
+ "title": "Pricing — 3 column",
53
+ "type": "synced",
54
+ "ref": 451,
55
+ "slug": null,
56
+ "categories": ["pricing", "featured"],
57
+ "excerpt": "Three plan cards with monthly price and CTA.",
58
+ "score": 0.92
59
+ },
60
+ {
61
+ "title": "Pricing table (registered)",
62
+ "type": "registered",
63
+ "ref": null,
64
+ "slug": "linchpin/pricing-3col",
65
+ "categories": ["pricing"],
66
+ "excerpt": "Theme-provided 3-tier layout.",
67
+ "score": 0.78
68
+ }
69
+ ]
70
+ }
71
+ ```
72
+ - `ref` is set for `synced` (the `wp_block` post ID → emit `wp:block {"ref":…}`).
73
+ - `slug` is set for `registered` and `template_part`.
74
+
75
+ ## `get_pattern`
76
+
77
+ > Fetch the full block markup for one match. For synced patterns prefer emitting the
78
+ > reference block; only fetch expanded markup when detaching/editing a single instance.
79
+
80
+ ```json
81
+ {
82
+ "name": "get_pattern",
83
+ "description": "Return the full block markup for one pattern or template part, by ref or slug.",
84
+ "parameters": {
85
+ "type": "object",
86
+ "properties": {
87
+ "ref": { "type": "integer", "description": "wp_block post ID for a synced pattern." },
88
+ "slug": { "type": "string", "description": "Slug for a registered pattern or template part." }
89
+ },
90
+ "oneOf": [{ "required": ["ref"] }, { "required": ["slug"] }]
91
+ }
92
+ }
93
+ ```
94
+ **Returns:** `{ "title": string, "type": "synced"|"registered"|"template_part", "block_markup": string, "ref": int|null, "slug": string|null }`
95
+
96
+ ## `list_template_parts`
97
+
98
+ ```json
99
+ {
100
+ "name": "list_template_parts",
101
+ "description": "List the active block theme's template parts (FSE). For template editing, not post body.",
102
+ "parameters": { "type": "object", "properties": {} }
103
+ }
104
+ ```
105
+ **Returns:** `{ "parts": [{ "slug": string, "title": string, "area": "header"|"footer"|"uncategorized"|string, "theme": string }] }`
106
+
107
+ ## `list_registered_blocks`
108
+
109
+ > Confirm a non-core block actually exists on this install before referencing it.
110
+
111
+ ```json
112
+ {
113
+ "name": "list_registered_blocks",
114
+ "description": "List blocks registered on this site. Filter by namespace to check availability of a specific block.",
115
+ "parameters": {
116
+ "type": "object",
117
+ "properties": {
118
+ "namespace": { "type": "string", "description": "e.g. 'linchpin' or 'core'. Omit for all." }
119
+ }
120
+ }
121
+ }
122
+ ```
123
+ **Returns:** `{ "blocks": [{ "name": string, "title": string, "category": string, "supports_inner_blocks": boolean }] }`
124
+
125
+ ## `get_theme_tokens`
126
+
127
+ > Pull theme.json design tokens so generated markup matches the site's design system.
128
+
129
+ ```json
130
+ {
131
+ "name": "get_theme_tokens",
132
+ "description": "Return the active theme's design tokens (colors, gradients, font sizes, spacing, layout widths) from theme.json.",
133
+ "parameters": { "type": "object", "properties": {} }
134
+ }
135
+ ```
136
+ **Returns:**
137
+ ```json
138
+ {
139
+ "colors": [{ "slug": "primary", "name": "Primary", "color": "#0b5fff" }],
140
+ "gradients": [{ "slug": "brand", "name": "Brand", "gradient": "linear-gradient(...)" }],
141
+ "font_sizes": [{ "slug": "large", "name": "Large", "size": "1.5rem" }],
142
+ "spacing": { "spacingSizes": [{ "slug": "40", "name": "Large", "size": "2rem" }] },
143
+ "layout": { "contentSize": "840px", "wideSize": "1200px" }
144
+ }
145
+ ```
146
+
147
+ ## `validate_blocks` (required before insert)
148
+
149
+ ```json
150
+ {
151
+ "name": "validate_blocks",
152
+ "description": "Parse block markup the way the editor does and report validity. Optionally return a normalized/repaired version.",
153
+ "parameters": {
154
+ "type": "object",
155
+ "properties": {
156
+ "block_markup": { "type": "string" },
157
+ "autofix": { "type": "boolean", "default": true, "description": "Return a normalized version with corrected wrappers/classes where possible." }
158
+ },
159
+ "required": ["block_markup"]
160
+ }
161
+ }
162
+ ```
163
+ **Returns:** `{ "valid": boolean, "issues": [{ "block": string, "message": string }], "fixed_markup": string|null }`
164
+
165
+ > The host already has block validators available (e.g. the WordPress Studio MCP
166
+ > `validate_and_fix_blocks` / `validate_html_blocks` tools) — wire `validate_blocks` to one of
167
+ > those rather than reimplementing the parser.
@@ -0,0 +1,38 @@
1
+ # Validation — always, before insert
2
+
3
+ Block markup that *looks* right can still be rejected by the editor (a drifted wrapper class,
4
+ a void/open mismatch, attribute JSON that doesn't match the saved HTML). A rejected block
5
+ shows as *"This block contains unexpected or invalid content"* and the user loses the layout.
6
+ So validation is not optional — it's the last step of every content generation.
7
+
8
+ ## The contract
9
+
10
+ 1. Generate the block markup (reused asset or composed core blocks).
11
+ 2. Call `validate_blocks({ block_markup, autofix: true })` (see `tool-contract.md`).
12
+ 3. Branch on the result:
13
+ - `valid: true` → insert the markup (use `fixed_markup` if present — it's normalized).
14
+ - `valid: false` → read `issues`, repair, and re-validate. Common repairs:
15
+ - void/open mismatch (`/-->` vs `-->`) — see `block-grammar.md`
16
+ - missing/wrong `wp-block-*` wrapper class or tag
17
+ - attribute JSON not matching generated classes (e.g. `textColor` set but `has-…-color`
18
+ class missing)
19
+ - a referenced block that isn't registered (`list_registered_blocks` to confirm) — drop
20
+ to a core-block equivalent
21
+ - Still invalid after a repair pass → **simplify**. Fall back to a plainer composition
22
+ (e.g. paragraphs + a single columns block) rather than shipping invalid markup. A simpler
23
+ layout that renders beats a fancy one that breaks.
24
+ 4. Never return un-validated markup to the editor, and never paper over failure with prose.
25
+
26
+ ## Why repair instead of guess
27
+
28
+ Saved-HTML shapes drift across WordPress/Gutenberg versions, so the exact classes in this
29
+ skill's examples won't be byte-perfect on every site. The validator reflects *this* site's
30
+ actual block versions — trust it over hand-written markup. Prefer `fixed_markup` from the
31
+ validator when it's offered; it's the site's own normalization of your intent.
32
+
33
+ ## Host wiring
34
+
35
+ Point `validate_blocks` at a real WordPress parser so "valid" means what the editor means.
36
+ The WordPress Studio MCP server exposes `validate_html_blocks` and `validate_and_fix_blocks`
37
+ for exactly this; reuse one instead of writing a parser. The `autofix`/`fixed_markup` path
38
+ maps to `validate_and_fix_blocks`.