chore: merge site-editor branch and cut 1.0.0-rc3 - #88
Merged
Conversation
Free Saddle can now update itself from updates.plugpress.co when installed from plugpress.co, and makes no outbound request at all in the WordPress.org build. - includes/class-saddle-updater.php: minimal update client. Sends slug and version only, 6h cache, 15min negative cache, admin/cron only. Offers an update only when the version is newer AND a signed package came back. - The .org switch is the file's ABSENCE, not a constant: saddle.php requires it behind file_exists(), the build task drops it for the wporg channel, and an install that later updates onto a .org zip falls back to core natively. - Init is scoped to is_admin() || wp_doing_cron(). Cron is not optional here: wp_update_plugins runs as a scheduled event, so an is_admin()-only guard would have silently killed background updates. - Gruntfile: two channels. `grunt build` excludes the updater (.org safe by default); --channel=selfhosted includes it and names the zip -selfhosted so the two artifacts cannot be confused. - Gruntfile: the version task accepts prerelease suffixes (1.0.0-rc1). It hard-rejected them before, so the rc scheme could not have been used at all. currentVersion() also had a numeric-only capture that named a 1.0.0-rc1 build "saddle-1.0.0.zip" - two builds, one filename. Stable tag never moves for a prerelease. Version stamped 1.0.0-rc1. Stable tag stays 1.0.0.
CLAUDE.md rewritten from scratch (444 -> ~600 lines), dated 2026-08-12: - Dropped the phase roadmap and every issue link, plus the pointer to BUILD-GUIDE.md, which had already been deleted from the tree while line 5 still told every agent to read it first. That deletion is committed here. - Added the hard line on code execution and filesystem writes, after auditing Novamira (free ships execute-php, run-wp-cli and write-file behind a single capability check, with no dry-run, diff or revert). - Added WordPress plugin rules, a Testing section including both Plugin Check traps, and a Direction section replacing the old roadmap. - Non-negotiable #1 gains a narrow, explicit carve-out for the update check, because the absolute wording became false when the updater shipped. - Version rules cover the -rcN lane and the two build channels. - Ability count corrected 63 -> 61. The 63 came from a grep that matched ability references inside descriptions, not registrations. AGENTS.md re-pointed: it advertised a GitHub workflow that the rewrite had briefly removed. STATUS.md: full session entry covering the positioning work, the block-theme gap, the Novamira audit, the update channel, and Mark Roach's remaining ChatGPT bug (diagnosed, not fixed, and not caused by the WP.org review).
…theme Saddle could read a post's blocks and nothing else. Not the header, not the footer, not the palette the owner actually set. An agent asked to "match this site" had no way to look at the site. grep for wp_template / wp_global_styles returned zero hits across includes/ - this was greenfield, not a gap. New: includes/abilities/site-editor.php, four read-tier abilities - list-templates: page templates and parts, with whether each is still the theme's file or has been customised. Parts report their area, so finding the header does not depend on guessing slug conventions. - get-template: one template as an addressable node list, in the SAME shape get-blocks returns. Addresses mean one thing everywhere, which is what makes a future set-template possible. - get-global-styles: the owner's own choices, separate from what the theme ships. get-design-system shows the merged result; this shows whose it is. - list-patterns: the owner's saved patterns, synced vs unsynced. All four go through core's get_block_templates() / get_block_template() / WP_Theme_JSON_Resolver rather than querying the post types. Core merges theme FILES with database customisations; querying posts alone would silently miss every template the theme ships and nobody has edited. Fixed: bootstrap-design-system no longer no-ops on a block theme. It used to compute the spec, walk the owner through the approval gate, consume the single-use token, and THEN return applied:false telling them to go do it by hand in Appearance -> Editor -> Styles. Now it writes the palette, type scale and spacing into user global styles, which is exactly where the Site Editor reads from. Specifically: - Refuses BEFORE gating when there is nowhere to write (classic theme, no builder), so no confirmation is ever spent to be told no. - Merges, never replaces. The gate's own summary promises "existing tokens are not removed", so overwriting would have made the preview a lie. A second seed adds nothing and leaves the first accent intact - pinned by a test. - Creates the wp_global_styles post when absent (get_user_data_from_wp_global_ styles with $create_post), which is the fresh-site case this ability is FOR. Reading via get_user_data() alone would have written to post id 0 silently. - bind now covers WHERE it writes, not just what. The same spec can land in two stores, and a confirmation approved for one must not execute against the other. Permissions UI: new "Site editor" category rule, placed AFTER "Blocks & layout" on purpose - that rule matches 'block', so list-block-patterns and insert-block-pattern already live there and would have silently moved. Tests: new tests/site-editor-test.php (9), plus a merge-safety test on bootstrap. The host WordPress this suite runs against has NO block theme, so these all skipped on first run - green while verifying nothing. Added a minimal block-theme fixture under tests/fixtures/themes/ and a second theme root, so they actually execute. Suite 469 green, 1556 assertions, 1 pre-existing skip. Free abilities 61 -> 65. Template WRITES are deliberately not here: overwriting a shared template is destructive in a way editing one post is not, and that gate deserves its own round.
- .agents/ and .claude/skills/ hold the Saddle-specific security rules an agent needs when touching a request path. Project knowledge, so it belongs in the repo rather than one machine. - .gitignore: .claude/settings.local.json is per-machine permission config and must not be shared. - Gruntfile: exclude .agents/** explicitly. The zip was already clean, but only because of how the globber happens to treat dot-directories — .claude/** was excluded by name and .agents/** was not. Relying on incidental behaviour to keep dev config out of a WordPress.org release is not a control.
rc1 had become ambiguous. The build published to R2 predates today's site-editor work (61 abilities); the local tree carries it (65). Two different builds under one number is exactly what the -rcN lane exists to prevent, and publishing today's code as rc1 would have been worse than untidy: PHP's version_compare sees rc1 == rc1, so no existing install would ever have been offered the update. Stable tag stays 1.0.0 — the version task refuses to move it for a prerelease. NOT yet published to R2. rc2 should carry the tools/list session fix as well, so the customer waiting on it gets one build that solves his problem rather than two that do not.
…free Saddle already knew how to build a page properly. That knowledge was delivered as three bullets of context prose and scattered tool descriptions, so an agent had to reassemble the sequence itself every session and nothing noticed when it did not. Prompted by comparing against Webflow MCP 2.0, which ships the design system as structured data agents "query and obey". saddle/context-bundle — one call replaces five - Design system, the blocks worth using, the theme's patterns and the owner's saved ones, the site's templates and parts, the section recipes, the conventions. Composes the existing reads; re-implements nothing. - Cached two ways: a 12h TTL AND a content signature checked on every read, so a Site Editor or WP-CLI edit self-corrects without waiting for the TTL. - A summary rides the system context, capped at 1000 chars, so a session starts oriented before it calls anything. - saddle_context_bundle filter so Pro can swap in its Divi payload later. PAYLOAD DISCIPLINE, learned the hard way. The block catalog was deliberately excluded from the start (unbounded: 110 types on a bare Twenty Twenty-Five). Patterns were not, and the first Playground run came back at 28KB because that theme ships 109 of them. The unit test passed throughout — it asserted the blocks slice only. Now capped at 12 with a total and a pointer to the searchable tool, and the test asserts the WHOLE payload under 12KB. Real result: 11,434 bytes, 146ms cold, 0ms cached. The build-page playbook, bundled in free on block themes - The seven-step loop with the tool named at each step, the rules the server actually enforces, and what separates a designed page from a generated one. - Embeds Saddle_Context::design_numbers() VERBATIM rather than restating it, so the design bar cannot drift from its single source. Pinned by a test. Bundled skills can no longer offer a control that 404s set_enabled() and delete() both look a skill up in the CPT first, so neither can act on a filter-provided one. The Guidance UI rendered a switch and a Delete link for every skill regardless. Pro has shipped into that for a while; free bundling a skill would have exposed it to every user. Skills now carry a `builtin` flag and the UI renders a "Bundled" badge instead. Naming - list-patterns -> list-saved-patterns. Added this morning, never published, so no alias needed. It sat next to the existing list-block-patterns and they are different sets. - get-design-tokens KEPT (public API, WP.org review in flight) but its description now sends agents to get-design-system, of which it is a strict subset. Four stale cross-references updated with it. - Permissions: 'context' added to the Memory & skills rule so context-bundle groups with get-instructions instead of falling to "Other". Suite 476 green (1599 assertions, 1 pre-existing skip), lint clean, bundle rebuilt. Free abilities 65 -> 66. Verified live in Playground on Twenty Twenty-Five.
…pped Publishing a self-hosted build makes an absolute "nothing leaves your site" claim false for everyone who downloads it, so these went in before the rc2 build rather than after. readme.txt - "no data sent to us — ever" -> states what is actually guaranteed: content, credentials and tool-call traffic stay in your WordPress. - "Only four things ever make an outbound request, and each one is started by you" -> both halves had broken. There are five, and the fifth is unattended. Now says plainly that the WordPress.org copy makes NO outbound request at all, and the plugpress.co copy checks for its own updates: plugin name and version, one fixed address, at most every six hours. - The "Saddle has no servers" FAQ answer, same correction. - Real changelog entries for the site-editor reads, the block-theme design system fix, context-bundle and the build-page playbook. Onboarding.jsx - The first screen a new user ever sees promised "Nothing leaves your website". Now "Your content stays on your site", which is true of both builds and is the thing they actually care about. Highest-visibility instance of the claim. CLAUDE.md non-negotiable #1 already carries the precise wording all public copy has to match; this brings the shipped copy in line with it. The live troubleshooting doc was corrected in the same pass.
WordPress.org rejected the submission over generic and reserved prefixes. Our own 126 elements were already clean — every one of the flagged wp_mcp and mcp_adapter names came from includes/lib/wp-mcp/, the vendored WordPress MCP Adapter, which is 347 of 464 shipped files and about half the zip. It also carried the only error_log and fwrite calls in the tree and stood up a second, undeclared endpoint at /wp-json/mcp/mcp-adapter-default-server. So the .org build no longer ships it. Nothing is lost: Saddle_MCP's own JSON-RPC transport already served the same /saddle/v1/mcp URL with the same abilities behind the same tier and approval gate, and a site that installs the official MCP Adapter plugin gets the adapter path back automatically. Absence is the switch, the way it already is for the updater. The bundle loader moved into its own excluded file rather than staying a guarded branch: it declares the library's WP_MCP_DIR/WP_MCP_VERSION, and a scanner reading source cannot tell those two defines are unreachable — shipping the file only where the library ships means the .org build contains no reserved-prefix declaration at all. The session shim moved with it, having no purpose without the adapter. Hardening the built-in transport first, since it is now the only one .org installs get: - Tool names lose the namespace slash. saddle/list-posts is illegal in MCP and rejected outright by OpenAI's clients, which drop the whole list over one bad name — the exact "no callable actions" symptom, waiting to happen again. call_tool() accepts both forms so a cached name still resolves. - Tools carry title and behaviour hints. Every ability already declared readonly/destructive/idempotent via saddle_ability_meta(); the transport was throwing it away. An agent can now weigh a call before making it. - A refusal is a tool result with isError, not a JSON-RPC error. MCP reserves protocol errors for protocol faults, and several clients never show the model a JSON-RPC error — which meant our carefully written "the site is at the read access level, do not retry" text was reaching nobody. Verified on the built .org zip, not the dev tree: 114 files, zero includes/lib, zero wp_mcp, zero fwrite, zero error_log calls. Then run in WP Playground over real HTTP — initialize returns tools-only capabilities, a stateless tools/list returns 61 tools with zero illegal names, a read tool executes, and a write tool at the default read tier is refused as isError with the reason intact. Refs #82
…longs WordPress.org flagged get-instructions for handing out plugin inventory at the read tier. The literal claim was wrong — the ability has a permission_callback — but the point underneath was right, and worse than they could see. site.php already states the policy: "Reads that expose configuration (option values, installed inventory) sit at admin too, not read — the inventory itself is sensitive." saddle/list-plugins and saddle/list-themes are gated on exactly that. Meanwhile the system context handed every read-tier session a prose copy of the same active-plugin list, with versions, plus the theme by name. So the plugin list and the theme name now appear only at the admin tier. What survives at every tier is the part an agent actually needs to avoid mangling a page: whether the theme is block-based, and the builder/multilingual detection, which is behavioural guidance rather than an inventory. The worse half was the handshake. initialize runs before any ability's permission_callback — its only check is that someone is logged in — and it served MORE than get-instructions would (the entire context plus the owner's own written instructions) with FEWER checks. A paused site still answered it, and a Subscriber-level key or a saddle:read bearer got the same payload as an admin. It now honours the pause switch, and its context is tier-aware, so the handshake can no longer exceed what the equivalent ability would return. Tests drive the real path rather than restating it: get_plugins() has no filter, so the plugin case primes the cache group it reads from, and asserts the same plugin is named at admin tier and absent at read.
…dropped readme.txt claimed Saddle "bundles the WordPress MCP Adapter library... license included in includes/lib/wp-mcp/". That stopped being true one commit ago, and a false statement in the readme is exactly the kind of thing a reviewer checks. Rewritten as what is now the case, and as the answer to the question the change raises: Saddle speaks MCP itself, there is nothing else to install, and if the separate MCP Adapter plugin is active Saddle uses it — same address, same tools, same access levels either way. Same wording in the Connections HelpTip, which called the built-in path a fallback, and in docs/connections.md. Also closes the troubleshooting gap that started this whole thread: the docs covered "connected but can't do something" and had nothing for "connected but shows no tools at all" — the exact sentence the customer sent twice. It now points at Client traffic and explains how to read it, including the case only that panel can show, where no row appears because the request never reached WordPress at all. WPORG-SUBMISSION.md: §3 no longer describes a bundled library, and the argument in §11 that the flagged notices were "unreachable in the shipped zip, not patched deliberately" is struck — they are simply not shipped. Records that the two remaining mcp_adapter_* strings are add_action/add_filter calls against that plugin's own hooks, which cannot carry our prefix. POT regenerated (1087 to 1090 msgids), bundle rebuilt.
…testing Throwaway build branch only — NOT for merge. Combines fix/wporg-rejection (off main) with feat/site-editor-reads so an rc3 test build is strictly ahead of rc2 rather than silently dropping the site-editor abilities and the self-hosted updater, which main does not yet have. Both generated bundle artifacts were rebuilt from the merged source rather than hand-resolved.
Combines the WordPress.org work with the rc2 branch so this build is strictly ahead of rc2. Self-hosted channel, so the updater ships and testers can be moved forward again. Stable tag deliberately stays at 1.0.0 — version_compare sorts a prerelease below the real release, so every rc install upgrades cleanly onto it. Deliberately built WITHOUT the vendored adapter, on both channels: the point of this build is to exercise Saddle's own transport, which is what WordPress.org installs will use and what the ChatGPT fix now runs through.
# Conflicts: # admin/build/index.asset.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings
feat/site-editor-readsand the WordPress.org compliance work together onmain, and sets the version to 1.0.0-rc3 for the self-hosted test release.What
Three streams land together:
Why
maindid not have the updater or the site-editor abilities, so an rc3 built frommainalone would have sorted above rc2 inversion_comparewhile silently removing both. Anyone updating would have lost the site-editor tools and, worse, the updater itself.How
Merge rather than squash, so the site-editor branch keeps its history. The only conflicts were the two generated bundle artifacts (
admin/build/index.js,index.asset.php), rebuilt from merged source rather than hand-resolved.Testing
composer test— 513 tests, 1817 assertions, 1 pre-existing skip, green (496 + 17 from the site-editor branch)composer lintandnpm run lint:js— both exit 0wp_mcp/WP_MCP/fwrite/error_login PHPNote
The rc3 self-hosted zip is built without the vendored adapter, on both channels. That was deliberate for a test build meant to exercise Saddle's own transport, and it is what shipped. The Gruntfile comment says the self-hosted channel should re-include it; that is still outstanding and worth doing before a general release.
mainmoves to 1.0.0-rc3.Stable tagstays at 1.0.0, so the WordPress.org submission is unaffected.