Fix round 1 (task 10 review):
- FieldValidator's "unknown field" validation error (reaches the
client via validation_failed's fields) and PostPresenter's
DIVI_HINT (surfaced in the result's hints array) still named
lit-content-types/lit-content-update as plain lit-<name>; converted
both to the neutral "lit/<name> (direkt: lit-<name>)" form like the
other hints from task 10. Re-grepped src/ for any other client-facing
error/field/hints text naming a direct-by-default tool as lit-<name>
(descriptions/schemas excluded per Q11c) - none left.
- Added Limits::bounds() (field => [min, max]) and made LimitsTab use
it for the number inputs' min/max instead of a second, hardcoded
copy of the same bounds.
- LimitsTab now shows a description note when the configured upload_mb
exceeds wp_max_upload_size() (PHP's upload_max_filesize/post_max_size),
since WordPress would refuse a larger upload before this limit ever
applies.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Adds Einstellungen → LIT MCP → Tools (direct/indirect/disabled per
ability, only ExposurePolicy-allowed modes offered) and → Limits
(upload size, log retention, snapshot count), wired into Plugin.php.
Ruling Q11b (carried from task 6): overview.md notes that direct
visibility is configurable; hints in src/ that name a tool which is
direct by default but can be switched to indirect (content-get,
content-query, content-types, post-publish, media-search) now use the
neutral "lit/<name> (direkt: lit-<name>)" form so they stay correct
after an admin override. lit/guide (always direct) and the
media-preview/-download DIRECT_ONLY tools are unaffected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
I1: query the attachments themselves author-restricted when the user
lacks edit_others_posts (PostGuard::ownPostsOnly, extracted from
ContentQueryAbility so content-query reuses the exact same rule)
instead of relying solely on a post-query editable() filter, which
could leak the existence of other users' attachments via an empty
page plus next_cursor. editable() stays as a backstop.
Also: M1 document that only status=inherit attachments are listed;
M2 tie-break ordering by ID (content-query and media-search) for
stable pagination; M3 no next_cursor once the last candidate was
examined; M4 reject an explicit non-image mime with missing_alt;
M5 additional coverage (posts_per_page, cursor offset pass-through,
invalid cursor, post_status/date/mime args); mime maxLength 100.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Usage search distinguishes certain (ID referenced) and possible (file URL
only) usages, hides titles the user cannot read, and keeps queries bounded.
MediaGuard checks per-attachment capabilities and refuses trashed media
unless the caller opts in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Indirect abilities are written lit/<name> (via mcp-adapter-execute-ability) in
guides, descriptions and hints, direct tools lit-<name>; guides use the
client-visible codes (write_failed, partial_write) and say exactly which tools
return a revision_id. ToolNamingTest keeps guides and sources consistent with
Ability::isDirect() and checks the overview's direct-tool list.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
- isStored() compares the stored option with sanitize_option() of the new value
(core esc_html()s blogname/blogdescription): setting "Müller & Söhne" again
no longer looks like a failed write; lit/wp-settings-get and previous return
those two decoded, as the user would type them.
- The change log runs as FollowUp (warning on failure); a failed write throws
WriteFailed (write_failed / partial_write with keys), partial writes are
logged with result partial.
- permalink_structure needs a real %tag% (WordPress' own /%[^/%]+%/ rule).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Fields from the meta/sample sources need edit_post_meta (update, checked in
PostWriter::prepare) or add_post_meta (create, checked right after the insert;
a refusal deletes the new draft again) for their key; a refusal is a
fields.<key> validation error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
publish/unpublish/schedule/trash run their WordPress call through StatusWrite:
a throwing hook becomes WriteFailed (rollback incomplete, with post_id) and is
logged with result partial instead of surfacing as internal_error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
after_save hook, re-read, change log and ToolResult (rev, log_id, warnings)
were repeated in nine post abilities; they now share one helper. Behaviour
and warning texts are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
- PostGuard::editable() refuses trashed entries unless the caller opts in
(content-get, revisions-list, post-trash); publish/schedule/unpublish/update/
restore/duplicate/replace now refuse them.
- PostGuard::statusStable(): content-update, revision-restore and
replace-with-copy refuse a future post whose date has passed (any save would
publish it); the content-update rollback and PostCloner row writes re-check.
- PostCloner::ownMeta() treats a failed SELECT as an error; replace() reads the
copy before its first write.
- replace-with-copy needs create_posts (backup clone) and reports backup_id in
write_failed/partial_write.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
recordPartial() replaces the partial-log FollowUp blocks in content-create,
content-update and post-replace-with-copy; record() truncates ability,
object_type and object_id to their column sizes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
RevisionRestoreAbility now wraps the after-save action and change-log
write in FollowUp::run() and reports failures as warnings instead of
letting them turn a saved restore into an error response, matching
ContentUpdateAbility; a failed wp_restore_post_revision() is now
Errors::writeFailed() (nothing was changed) instead of unsupported().
RevisionsListAbility marks each item divi (reusing
PostPresenter::usesDivi()/DIVI_HINT) since a Divi/block revision's
excerpt is stripped markup, not prose.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
WriteFailed maps to lit_write_failed / lit_partial_write (partial writes are
logged with result partial); old template read before the update; row marker
set before wp_update_post; created terms deleted on rollback; parent must be a
readable non-descendant of a hierarchical type; featured image must be
readable; snapshot keeps empty values; meta comparison after sanitize_meta;
follow-up failures (listener, change log) only add warnings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
All-or-nothing writes: validation (fields, terms, featured image, parent,
template) before any write; failed extras delete the new draft (create) or
restore revision, old columns, fields, terms and featured image (update).
content-update requires rev; Rev::ofPost covers content, title, excerpt,
status and post_modified_gmt. MetaFieldSource::write() now throws on failure.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Review fix round 2: content-query no longer lets get_posts() fall back
to 'publish' when every requested status is capability-gated out (now
short-circuits to an empty page). Its field filter shares PostPresenter's
hidden-field rule (Pods password type, protected meta) so a hidden key
is rejected like an unknown one instead of reaching the query. Password
fields are now type-checked as strings on write. A user without
edit_private_posts can still query their own private posts when the
query is already author-restricted, matching WordPress core.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
Review fix round 1: content-query now excludes posts/statuses the user
cannot edit before querying (author restriction without edit_others_posts,
status filtering by edit_private_posts/edit_published_posts) instead of
only dropping them after fetching, and rejects unknown field/taxonomy
filters. content-types' not-found hint only lists editable types.
PostPresenter skips Pods password fields and readonly protected meta.
Query input gains search/terms length limits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7