Skip to content

Commit 3b573df

Browse files
ifahimrezaclaude
andauthored
feat(playbook): ship the playbooks on every site that can use them (#95) (#96)
* fix(guardrails): hold a wrapped destructive confirm to the args it previewed The gate's `bind` parameter exists to fold confirmation-relevant arguments into the token's identity, and the integration wrappers were not passing it. The only thing tying a confirm to its preview was `target` — the first of id/post_id/attachment_id in the input — so on any partner tool taking an id plus a payload, every other argument was unbound and the delegate closed over the *confirm* call's input. Preview {post_id: 12, description: "approved"}, confirm {post_id: 12, description: "something else"}, and the second one ran. The user approved one change and got another, which is the two-step confirm doing the opposite of its job. Now `bind` is a sha256 of the whole argument set minus confirm_token, key sorted so a client reordering its JSON doesn't break a legitimate confirm. `target` stays the bare id, so the log line is still a readable "#12" rather than a hash. The target key list is widened past the original three and is filterable — a key missing from it now costs legibility, never safety. Also here: denial_reason() had no branch for a capability denial, the one refusal whose fix is a different WordPress account rather than anything in the Saddle dashboard. It fell through to the generic paragraph, which lists three fixes and not that one. permission() now records the capability each ability asks for as it registers — one line, and it covers free, Pro and the wrappers alike, because all three build their callback there. Closes #89 Refs #63 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkZr73cqaSesHRDG89Yy8S * feat(tools): offer only the tools this credential can actually call A fresh install sits at the read tier — that is the second non-negotiable — and then hands the agent all 66 free tools, 30 of which are refused on every single call. With Pro and the first-party integrations on, the same list runs to about 160. Every one of those unusable entries costs a full JSON schema in the agent's context window, and buys a guaranteed refusal. tools/list is now filtered to what the current credential could execute: the effective tier (already clamped by any OAuth scope), the owner's per-tool switches, and the capability the connected account holds. Both transports — and on a WordPress.org install the built-in one is the only transport there is, since the adapter stopped shipping in that zip. The filter runs at DISPATCH, never at registration. adapter_tool_names() is built during mcp_adapter_init, inside rest_api_init and before authentication; filtering there would read the wrong tier and force early user resolution on every REST request site-wide. So the adapter server is still registered with the complete set and mcp_adapter_tools_list narrows the response. Pause is deliberately excluded. It denies everything at call time anyway, and emptying the list would make every connected client reconnect on resume. The instructions lead with the pause warning instead. Filtering costs an agent the ability to say "that tool exists, you just have not enabled it", so that is bought back explicitly: the context now says how many tools are withheld and by which gate, and get-site-info reports the same counts. Two sentences instead of thirty schemas, and it is the half a user can act on. Along the way: is_callable_now() resolves the ability before reading the capability registry, because wp_get_abilities() is what lazily fires wp_abilities_api_init and fills it — reading it first silently skips the check on the first call of a request. Pinned by a test. The POT was regenerated and had drifted well past this change: it was missing context-bundle, list-templates, get-template and the block-theme bootstrap-design-system strings entirely — an entire feature's worth of msgids, which is the exact failure CLAUDE.md warns about. Refs #89 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkZr73cqaSesHRDG89Yy8S * fix(context): point at the tool that replaced five, and stop stuttering Three things the context was getting wrong, all of them cheap to fix and paid on every single session. **It never mentioned context-bundle.** The bundle exists precisely to collapse get-design-system + list-block-types + list-block-patterns + list-section-recipes + list-templates into one call, and the steering was still describing that five-call sequence — so every session paid the calls the bundle was built to save. The design section now leads with ORIENT FIRST, and closes the loop the playbook teaches: verify-page, then actually open the preview. **Saddle_Context_Bundle::summary_lines() was dead code.** Computed, budgeted, and documented in its own docblock as riding the system context "so a session starts oriented before it calls anything at all" — and called by nothing since the bundle shipped. Now hooked, under its own heading, with its self-referential prefix and its "call context-bundle" tail dropped: the context supplies both and would otherwise say them twice. **Recent changes stuttered.** A real site was spending six of its fifteen lines on six consecutive saves of one post, which tells an agent nothing the first line didn't. Consecutive entries on the same action and target now fold into one line with a count, keeping the NEWEST summary — the current title, not the draft one. Only consecutive runs fold, so editing A, then B, then A again still reads as three steps. saddle/recall-changes stays unfolded; it is the audit view. Refs #91 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkZr73cqaSesHRDG89Yy8S * feat(playbook): ship the playbooks on every site that can use them **build-page was gated on wp_is_block_theme().** The real question is not "block theme?" but "does something else own these pages?" — a classic theme still edits its posts and pages in the block editor, and it is the one kind of site with no Site Editor to fall back on. So the sites most in need of a playbook were precisely the ones getting none. The disqualifier is a FOREIGN builder: its pages are markup inside the content and the native block tools refuse them, so a Gutenberg playbook there is worse than saying nothing. Divi with Saddle Pro is not foreign — Pro declares it through saddle_native_builders and bundles its own playbook, which shadows by name. That native/foreign split already existed inside Saddle_Context as private logic; it is now two named methods, so the playbook and the context answer the question the same way instead of twice. Step 2 adapts: on a classic theme there are no template parts to read, so it sends the agent to get-blocks on an existing page rather than after get-template, which would be refused. **And a second playbook, fix-page.** The repair loop was one bullet inside build-page step 6, and it is the flow an agent hits most often — usually out of order. It now has room to say why the order is the order (structural first because everything else is measured against a tree that is about to change, then "ignored" because styling that never landed looks identical to styling that did), and to be blunt about the trap that damages pages: addresses are positional, so one structural edit shifts every address after it. Make one change, re-read, then make the next. Four skills tests had assumed the classic test site had no built-ins at all — which was true only because of the gate this commit removes. They now measure what they mean (owner-installed skills), and there are five new cases covering the gate in both directions and the classic-theme step 2. Refs #93 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkZr73cqaSesHRDG89Yy8S * docs: commit the agent guide rewrite, and name the four pillars Two things, both about the guide rather than the code. **The 2026-08-12 rewrite was never committed.** It has been sitting in the working tree since that session — the re-derived CLAUDE.md, the ROADMAP.md split out of it (a dated roadmap inside a file that loads on every task goes stale and misleads mid-session), and AGENTS.md turned into a symlink so the two never drift. Recovered as-is; none of it is my wording. **Plus a new section: the four pillars.** Context, guardrails, tools, and the system — what a connected agent gets, where each part lives, and the seams between them. The work in this branch kept turning up features that had landed *beside* a pillar rather than in one: a bundle summary computed and never served, steering that described the five calls the bundle replaced, a tool list that ignored the tier, a gate parameter that existed and was not passed. Naming the four is how the next feature lands in one. One correction to the roadmap while committing it: item 1, "site-editor reads", has already shipped — list-templates, get-template, get-global-styles and list-saved-patterns are all registered in includes/abilities/site-editor.php. The rest of that file still needs re-deriving against the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkZr73cqaSesHRDG89Yy8S --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 3aff80d commit 3b573df

8 files changed

Lines changed: 829 additions & 426 deletions

File tree

AGENTS.md

Lines changed: 0 additions & 24 deletions
This file was deleted.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

CLAUDE.md

Lines changed: 365 additions & 326 deletions
Large diffs are not rendered by default.

ROADMAP.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Saddle — direction
2+
3+
Moved out of `CLAUDE.md` on 2026-08-12. It was a dated section in a file that
4+
loads on every task; roadmaps belong where they can go stale without misleading
5+
anyone mid-session.
6+
7+
Each item becomes a GitHub issue when it's picked up. The positioning narrative
8+
behind it is in `STATUS.md` (2026-08-12 entry).
9+
10+
**Re-derived 2026-08-15: items 1 and 2 have SHIPPED.** The text below was
11+
written before they did; it is kept because items 3–6 still stand on it. Read
12+
the gap description as history, not as the current state.
13+
14+
## The gap that matters — as it stood on 2026-08-12
15+
16+
**On a block theme, Saddle can build a page but not a site.** Verified against
17+
the tree: there are zero abilities for templates, template parts, global styles,
18+
user patterns or fonts, and no reference anywhere in `includes/` to
19+
`wp_template`, `wp_global_styles`, `wp_font_family` or `wp_block`.
20+
21+
Worse, `saddle/bootstrap-design-system` **silently no-ops on block themes**
22+
(`includes/abilities/blocks.php`): it returns `applied: false` and tells the
23+
owner to go do it by hand in Appearance → Editor → Styles. Only Divi, via Pro's
24+
filter, gets a design system actually written. An agent on a block-theme site
25+
cannot see the header, cannot set the palette, and cannot save what it built as
26+
a reusable pattern.
27+
28+
## Ordered, and deliberately split by risk
29+
30+
1. ~~**Site-editor reads (free, DB-only).**~~ **SHIPPED.** `list-templates`,
31+
`get-template`, `get-global-styles` and `list-saved-patterns` are registered
32+
in `includes/abilities/site-editor.php`, read tier. `list-fonts` was not
33+
built and nobody has asked for it.
34+
2. ~~**Make `bootstrap-design-system` real on block themes.**~~ **SHIPPED.**
35+
`includes/abilities/blocks.php` resolves a `$store` — builder, global-styles,
36+
or none — *before* the gate, so a classic-theme site is refused up front with
37+
a reason instead of spending a single-use token to be told to go do it by
38+
hand. The block-theme path writes the spec into global styles.
39+
3. **Template / part / pattern writes (free, DB-only).** `set-template`,
40+
`create-template-part`, and "save this subtree as a pattern" built on
41+
`Saddle_Tree`. Approval-gated on overwrite.
42+
4. **Filesystem export — a separate addon, never free.** The block-theme
43+
equivalent of Create Block Theme, agent-driven: plan → export → clean, moving
44+
templates, parts, global styles, patterns and fonts out of the database and
45+
into theme files so an agency can version-control them. Native PHP via
46+
`WP_Filesystem`**not** a bash or WP-CLI wrapper (see the hard line in
47+
`CLAUDE.md`). The `clean` step is exactly what `Saddle_Approval::gate()` was
48+
built for.
49+
5. **If code-writing ever happens, it is that same addon, and CSS first.** A
50+
child theme stylesheet or Additional CSS is non-executable, covers most "make
51+
it look right" work, and pairs directly with `get-design-system`. Data files
52+
(`templates/*.html`, `theme.json`) next. PHP templates only if demand proves
53+
it out. Every one of them goes through diff-preview → confirm → `saddle_log`
54+
revertable, which is the whole differentiator.
55+
6. **The Divi analogue belongs in Pro.** Theme Builder templates, Global Presets
56+
and Global Colors all live in the database with no version-control story. Pro
57+
already reads all three; a JSON export/import into a repo is the direct
58+
parallel.
59+
60+
## Constraint on all of the above
61+
62+
Nothing that adds a filesystem write ships in free while the WordPress.org
63+
submission is in flight.

includes/class-saddle-context.php

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -182,18 +182,8 @@ public static function system_context() {
182182
}
183183

184184
if ( ! empty( $builders ) ) {
185-
/**
186-
* Filter the builders whose pages have DEDICATED saddle tools
187-
* installed (e.g. Saddle Pro registers 'Divi'). Native builders
188-
* get an in-scope note instead of the hands-off warning — an
189-
* addon that ships a full editing surface must not have the
190-
* base plugin telling agents to leave those pages alone.
191-
*
192-
* @param string[] $native Builder labels (as detected, e.g. 'Divi').
193-
*/
194-
$native = array_map( 'strval', (array) apply_filters( 'saddle_native_builders', array() ) );
195-
$in_tool = array_values( array_intersect( $builders, $native ) );
196-
$foreign = array_values( array_diff( $builders, $native ) );
185+
$in_tool = self::native_builders();
186+
$foreign = self::foreign_builders();
197187

198188
if ( ! empty( $in_tool ) ) {
199189
$lines[] = sprintf(
@@ -269,6 +259,47 @@ public static function system_context() {
269259
return (string) apply_filters( 'saddle_system_context', $context, $tier );
270260
}
271261

262+
/**
263+
* Active page builders that have DEDICATED saddle tools — Saddle Pro
264+
* registers 'Divi'. Their pages are in scope and are edited through those
265+
* tools.
266+
*
267+
* @return string[] Builder labels.
268+
*/
269+
public static function native_builders() {
270+
return array_values( array_intersect( self::detect_signals( self::builder_signals() ), self::declared_native() ) );
271+
}
272+
273+
/**
274+
* Active page builders NOTHING here can edit. Their pages store layout as
275+
* markup inside the content, so the native block tools must stay away —
276+
* which is also what decides whether the bundled Gutenberg playbook makes
277+
* sense on this site.
278+
*
279+
* @return string[] Builder labels.
280+
*/
281+
public static function foreign_builders() {
282+
return array_values( array_diff( self::detect_signals( self::builder_signals() ), self::declared_native() ) );
283+
}
284+
285+
/**
286+
* The builder labels an addon has claimed.
287+
*
288+
* @return string[]
289+
*/
290+
private static function declared_native() {
291+
/**
292+
* Filter the builders whose pages have DEDICATED saddle tools
293+
* installed (e.g. Saddle Pro registers 'Divi'). Native builders get an
294+
* in-scope note instead of the hands-off warning — an addon that ships
295+
* a full editing surface must not have the base plugin telling agents
296+
* to leave those pages alone.
297+
*
298+
* @param string[] $native Builder labels (as detected, e.g. 'Divi').
299+
*/
300+
return array_map( 'strval', (array) apply_filters( 'saddle_native_builders', array() ) );
301+
}
302+
272303
/**
273304
* The design-memory line: this site's palette, what wraps every page, and
274305
* how many ready-made patterns it has.

includes/class-saddle-playbook.php

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,81 @@ class Saddle_Playbook {
3030
* @return array[]
3131
*/
3232
public static function register( $skills ) {
33-
if ( ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() ) {
33+
// The real question is not "block theme?" but "does something else own
34+
// these pages?". A classic theme still edits its posts and pages in the
35+
// block editor, and gating on wp_is_block_theme() left exactly those
36+
// sites — the ones with no builder and no site editor to fall back on —
37+
// with no playbook at all. A FOREIGN builder is the disqualifier: its
38+
// pages are markup inside the content and the native block tools refuse
39+
// them, so a Gutenberg playbook would be worse than saying nothing.
40+
// Divi with Saddle Pro is not foreign, and Pro bundles its own.
41+
if ( class_exists( 'Saddle_Context' ) && Saddle_Context::foreign_builders() ) {
3442
return $skills;
3543
}
3644

3745
$skills[] = array(
3846
'name' => 'build-page',
3947
'description' => __( 'Build or edit a page end-to-end with real editor blocks: orient once with context-bundle, plan from a pattern or recipe, build against the site\'s own design system, then verify and look at the result before calling it done.', 'saddle' ),
40-
'when_to_use' => __( 'creating, rebuilding, or restyling any page or post on a block theme', 'saddle' ),
48+
'when_to_use' => __( 'creating, rebuilding, or restyling any page or post', 'saddle' ),
4149
'source' => 'saddle',
4250
'body' => self::body(),
4351
);
4452

53+
$skills[] = array(
54+
'name' => 'fix-page',
55+
'description' => __( 'Work a verify-page report down to nothing: which findings to fix first, why block addresses move under you after a structural edit, and when the page is actually done.', 'saddle' ),
56+
'when_to_use' => __( 'a page exists and verify-page or lint-page reported findings to fix', 'saddle' ),
57+
'source' => 'saddle',
58+
'body' => self::repair_body(),
59+
);
60+
4561
return $skills;
4662
}
4763

64+
/**
65+
* The repair loop.
66+
*
67+
* Split out of build-page step 6, where it was one bullet. Fixing a page
68+
* someone else built — or that you built three turns ago — is the flow an
69+
* agent hits most often and the one it most often runs out of order:
70+
* fixing warnings before structure, and re-using addresses that moved
71+
* under it when the structure changed.
72+
*
73+
* @return string Markdown.
74+
*/
75+
private static function repair_body() {
76+
return implode(
77+
"\n",
78+
array(
79+
__( '# Fixing a page from its verify report — agent playbook', 'saddle' ),
80+
'',
81+
__( 'saddle/verify-page returns findings, a letter grade and a coverage caveat. The grade is honest about its own limits: a structural finding caps it at C and an "ignored" finding at B, because both mean the page is not doing what the markup says it does. Work them in the order below — fixing warnings first wastes the effort when a structural fix moves everything.', 'saddle' ),
82+
'',
83+
__( '## The order, and why it is this order', 'saddle' ),
84+
'',
85+
__( '1. STRUCTURAL findings first. These say the block tree itself is wrong — something nested where it cannot be, or a container with nothing in it. Every other finding is measured against a tree that is about to change, so fixing anything else first is work you will redo.', 'saddle' ),
86+
__( '2. IGNORED findings next. An "ignored" finding means you set something that never took effect: an attribute the block does not have, a preset slug that matches nothing on this site, a style group the block does not support. The page looks unstyled and the markup looks fine, which is why this one is easy to miss and worth its own pass. The fix is almost always a slug from saddle/context-bundle instead of an invented value.', 'saddle' ),
87+
__( '3. ERRORS, then WARNINGS. By this point the tree is right and the styling is landing, so these are real judgements about the design rather than noise.', 'saddle' ),
88+
'',
89+
__( '## Addresses move — re-read before you address again', 'saddle' ),
90+
'',
91+
__( 'Findings are addressed the same way saddle/get-blocks addresses nodes. Those addresses are positional, so ANY structural edit — adding, removing or moving a block — shifts every address after it. An address you read before that edit now points somewhere else.', 'saddle' ),
92+
__( 'So: make one structural change, then call saddle/get-blocks again before touching the next one. Batching several structural edits against one read is the single most common way to damage a page while trying to fix it.', 'saddle' ),
93+
__( 'Non-structural edits (saddle/edit-block on attributes or text) do not move anything, so those can be batched safely.', 'saddle' ),
94+
'',
95+
__( '## Working the loop', 'saddle' ),
96+
'',
97+
__( '- Re-run saddle/verify-page after each pass. Stop when the score stops improving, not when it hits a number you like.', 'saddle' ),
98+
__( '- If a finding survives a fix you believe in, read the node with saddle/get-blocks and check what actually saved. A refused write leaves the page exactly as it was, so "I fixed it" and "it is fixed" are different claims.', 'saddle' ),
99+
__( '- saddle/lint-page is the design-quality half on its own, without the scoring. Use it when you want the findings but not the grade.', 'saddle' ),
100+
'',
101+
__( '## When it is done', 'saddle' ),
102+
'',
103+
__( 'No structural and no ignored findings, and you have opened saddle/get-preview-url and looked at the page. The report is server-side only and says so in its own coverage caveat — it cannot see overlap, contrast in context, or a section that is simply ugly. A page can score well and still be wrong, and that judgement is yours to make with your eyes.', 'saddle' ),
104+
)
105+
);
106+
}
107+
48108
/**
49109
* The playbook body.
50110
*
@@ -54,12 +114,30 @@ private static function body() {
54114
return implode( "\n", array_merge( self::head(), self::design_bar(), self::tail() ) );
55115
}
56116

117+
/**
118+
* Whether this site has block templates and parts to read.
119+
*
120+
* The playbook now ships on classic themes too, where step 2 — go look at
121+
* the header and footer — has nothing to point at, because they live in
122+
* theme PHP Saddle does not edit. Telling an agent to call get-template
123+
* there sends it after a tool that will refuse it.
124+
*
125+
* @return bool
126+
*/
127+
private static function has_block_templates() {
128+
return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
129+
}
130+
57131
/**
58132
* The workflow and the rules the server actually enforces.
59133
*
60134
* @return string[]
61135
*/
62136
private static function head() {
137+
$look = self::has_block_templates()
138+
? __( '2. LOOK AT THE SITE — saddle/get-template on the header and footer parts. What wraps every page tells you the width, the spacing and the tone you are designing into. Skipping this is how a page ends up looking bolted on.', 'saddle' )
139+
: __( '2. LOOK AT THE SITE — this is a classic theme, so the header and footer live in theme files Saddle does not read. Open saddle/get-blocks on an existing page instead: what is already there tells you the width, the spacing and the tone you are designing into. Skipping this is how a page ends up looking bolted on.', 'saddle' );
140+
63141
return array(
64142
__( '# Building a page in WordPress — agent playbook', 'saddle' ),
65143
'',
@@ -68,7 +146,7 @@ private static function head() {
68146
__( '## The workflow — in this order', 'saddle' ),
69147
'',
70148
__( '1. ORIENT — saddle/context-bundle. One call gives you the design system, the blocks worth using, the theme\'s patterns, the site\'s templates and the section recipes. Call it once per session, not per page.', 'saddle' ),
71-
__( '2. LOOK AT THE SITE — saddle/get-template on the header and footer parts. What wraps every page tells you the width, the spacing and the tone you are designing into. Skipping this is how a page ends up looking bolted on.', 'saddle' ),
149+
$look,
72150
__( '3. PLAN — pick a starting point before composing anything. A theme pattern (saddle/insert-block-pattern) beats a section recipe, because it already carries this theme\'s styling. A recipe (saddle/get-section-recipe) beats hand-stacking blocks. Hand-stacking is the last resort, not the default.', 'saddle' ),
73151
__( '4. BUILD — saddle/set-blocks for a whole page, saddle/add-block and saddle/edit-block for surgical changes. Read saddle/get-block-schema before using a block type you have not used: it carries the placement rules and a worked example.', 'saddle' ),
74152
__( '5. STYLE WITH THE SITE\'S OWN VALUES — use the slugs from the design system, never raw hex or pixel values. {"backgroundColor":"<slug>"}, {"textColor":"<slug>"}, {"fontSize":"<slug>"}. A page built from slugs follows the site when the owner changes their palette; a page built from hex values does not.', 'saddle' ),

0 commit comments

Comments
 (0)