Skip to content

chore: merge site-editor branch and cut 1.0.0-rc3 - #88

Merged
ifahimreza merged 13 commits into
mainfrom
build/rc3
Aug 15, 2026
Merged

chore: merge site-editor branch and cut 1.0.0-rc3#88
ifahimreza merged 13 commits into
mainfrom
build/rc3

Conversation

@ifahimreza

Copy link
Copy Markdown
Contributor

Brings feat/site-editor-reads and the WordPress.org compliance work together on main, and sets the version to 1.0.0-rc3 for the self-hosted test release.

What

Three streams land together:

  • The site-editor branch (7 commits, previously unmerged): site-editor reads, the self-hosted updater and its two build channels, the orient/playbook work, and the CLAUDE.md rewrite.
  • The WordPress.org work (already merged to main as fix: clear the WordPress.org rejection (#82) #87): the vendored MCP adapter dropped from the build, the built-in transport hardened, inventory tier-gated.
  • 1.0.0-rc3, built on the self-hosted channel.

Why

main did not have the updater or the site-editor abilities, so an rc3 built from main alone would have sorted above rc2 in version_compare while 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 test513 tests, 1817 assertions, 1 pre-existing skip, green (496 + 17 from the site-editor branch)
  • composer lint and npm run lint:js — both exit 0
  • Bundle rebuilt from merged source
  • rc3 zip audited: 101 files, updater and site-editor present, zero wp_mcp / WP_MCP / fwrite / error_log in PHP

Note

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.

main moves to 1.0.0-rc3. Stable tag stays at 1.0.0, so the WordPress.org submission is unaffected.

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
@ifahimreza
ifahimreza merged commit 86aa7d3 into main Aug 15, 2026
1 of 7 checks passed
@ifahimreza
ifahimreza deleted the build/rc3 branch August 16, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant