← StripoCONTENT HISTORY

Update to Stripo

Snapshot Nov 3, 2026 · 00:13 UTC · version 1.0.2

Collection source: downloaded plugin package.

WHAT CHANGED

Instructions updated for email-model-editor

Full description — Now says: email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block...

Observed in instructions or declared skills. Runtime behavior has not been tested.

Save Stripo and check future changes in Following.

Up to 10 plugins free · no card required.

All fields and original values.

Full description

Before

Oct 31, 2026 · 01:14 UTC

email or template through its native JSON model. Use for document title and preheader, copy, link, image-source, AI image generation/editing, theme, style, and block additions, removals, or duplication when the email or template has an e...

After

Nov 3, 2026 · 00:13 UTC

email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block additions, removals, or duplication. Uses validated SDK mutations, base/target merging a...

Skill instructions

Before

Oct 31, 2026 · 01:14 UTC

email or template through its native JSON model. Use for document title and preheader, copy, link, image-source, AI image generation/editing, theme, style, and block additions, removals, or duplication when the email or template has an e...

After

Nov 3, 2026 · 00:13 UTC

email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block additions, removals, or duplication. Uses validated SDK mutations, base/target merging a...

Supporting files

Before

Oct 31, 2026 · 01:14 UTC

[{"relative_path":"HOST.md","size_in_bytes":6991},{"relative_path":"PROVIDER.md","size_in_bytes":19378},{"relative_path":"agents/claude.yaml","size_in_bytes":286},{"relative_path":"agents/openai.yaml","size_in_bytes":640},{"relative_path...

After

Nov 3, 2026 · 00:13 UTC

[{"relative_path":"HOST.md","size_in_bytes":8320},{"relative_path":"PROVIDER.md","size_in_bytes":2596},{"relative_path":"agents/claude.yaml","size_in_bytes":286},{"relative_path":"agents/openai.yaml","size_in_bytes":328},{"relative_path"...

Compare saved observations

Download comparison JSON

SKILL.md text changes

− removed · + added

--- before
+++ after
@@ -1,40 +1,49 @@
 ---
 name: email-model-editor
-description: "Edit an existing Stripo email or template through its native JSON model. Use for document title and preheader, copy, link, image-source, AI image generation/editing, theme, style, and block additions, removals, or duplication when the email or template has an editor document state. Uses Stripo MCP image tools for requested visuals and a generated SDK mutation module, validated file-based execution, and durable read-back for model changes. Use email-from-reference for a new email or full rebuild. Do not use for raw-HTML editing, sending, scheduling, changes to name/project/folder, or content without a native model."
+description: "Edit an existing Stripo email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block additions, removals, or duplication. Uses validated SDK mutations, base/target merging and durable readback. Use email-from-reference for new emails/full rebuilds. Not for raw HTML, translation, sending/scheduling, external metadata (subject, sender, name, project or folder), or content without a native model."
 ---
 
 # Email Model Editor
 
-Host paths: in Claude Code `<skill-dir>` is `${CLAUDE_SKILL_DIR}` and `<bundle-root>` is
-`${CLAUDE_PLUGIN_ROOT}`. In Codex `<skill-dir>` is the directory of this `SKILL.md`. In both
-hosts `<bundle-root>` is two levels above `<skill-dir>`. Read `HOST.md` beside this file for
-the working directory and file-transfer commands before the first MCP call.
-
-Edit an existing Stripo email or template through its JSON model without flattening it to HTML.
-`PROVIDER.md` beside this file defines the Stripo MCP contract: reference and target resolution,
-model acquisition, image jobs, persistence, and verification. Follow it for every MCP
-step below; `<bundle-root>/mcp-tools.json` lists the tool names. The bundle contains no MCP
-endpoint or credential; use the Stripo MCP server and authorized file-transfer mechanism
-configured by the consuming agent.
-If the host installs `HOST.md` beside this file, read it for concrete file-transfer commands.
-The image workflow is supplied by `PROVIDER.md` and does not require a host image adapter.
-
-For an attached or Ctrl+V-pasted replacement image, follow [Upload supplied images](reference/image-upload.md)
-before editing the model. The host must supply the original bytes as a local file. Host that
-file with `prepare_image_upload`, the authorized HTTP transfer, and `upload_image`; use only
-the verified final `data.url`. Reacquire the target and check its current image source before
-replacement. Keep the original asset if hosting fails; do not regenerate a supplied image.
-
-When repairing a draft created from a reference, retain its chosen Exact, Tailored, or Creative
-regime and original PNG baseline. For Exact, preserve source choices, reuse existing assets or
-crops before considering AI generation, and compare the repaired PNGs with the original source.
-Do not reset the regime or adapt the branding during the repair.
-
-Validation runs the editor's own Document State rules bundled into the SDK. No schema download
-or initialization is required. `bundle.json.editorValidator` records the editor revision.
-`finish()` validates edits against the acquired input; create mode validates a new document.
-The service also validates its deployed revision and applies the write to its live state;
-local success does not establish persistence.
+Edit content, styles, and structure of an existing email without flattening the native model to
+HTML. Use `email-from-reference` for a new email or full rebuild.
+
+Reuse instructions already present, including a complete injected `SKILL.md` and shared topics
+across both email skills in the same provider/bundle. Reread missing, truncated or changed
+documents and reload on provider/version changes. All checks and remote-state freshness apply.
+
+Read [PROVIDER.md](PROVIDER.md) once before tool discovery for target resolution, metadata,
+transfers, images, persistence and verification. If installed, read `HOST.md` for bundle paths,
+transfer/attachment bindings, image viewing, permissions and delegated image operations.
+`mcp-tools.json` maps operation roles to advertised names; discover the selected server's actual
+schemas, not those logical keys. The bundle contains no endpoint or credentials.
+
+Use one agent by default and batch independent tool calls. For a newly acquired edit task, read
+the acquisition, mutation, persistence and [parallel work](reference/parallel-work.md) guides plus
+the provider's acquisition and persistence topics not already loaded, each in its own tool call.
+The parallel guide limits delegation to large independent subtasks or separate reviews. For a
+repair continuing creation, use the continuation below. Load conditional topics on demand,
+never the full tool/resource catalog.
+
+| Stage | Common guides read together; conditional topics when needed |
+| --- | --- |
+| Acquire | [Download and inspect the target](reference/acquire-and-inspect.md) before editing. |
+| Mutate | [SDK module, run, validation, completeness](reference/mutate-and-validate.md) before constructing the change. |
+| Conditional mutation | [Metadata, social, insertion, global block settings, theme resets and fonts](reference/mutation-options.md) when the request includes these operations. |
+| Text or colors | [Color consistency](reference/color-consistency.md) before changing text markup, colors, backgrounds, or inserting text. |
+| Editor limits | [Editor limitations](reference/editor-limitations.md) before changing text color, line height, or layout. |
+| Persist | [File upload, recovery, durable and visual checks, final presentation](reference/persist-and-verify.md) before writing. |
+| Attached or Ctrl+V image | [Upload supplied images](reference/image-upload.md) before hosting original bytes. |
+
+For an attached or Ctrl+V-pasted replacement image, the host must supply the original bytes as
+a local file. Host it before editing the model and use only the verified final `data.url`. Reacquire the
+target and check its current image source before replacement. Keep
+the original asset if hosting fails; do not regenerate a supplied image.
+
+## Continue after creation
+
+For a repair of the same saved draft, use [the continuation](reference/mutate-and-validate.md#continue-after-creation).
+Carry forward the latest readback and loaded contracts; keep all edit invariants and verification below.
 
 `<skill-dir>` is this installed skill directory. `<bundle-root>` is the directory containing the
 installed `skills/` and `packages/` directories.
@@ -45,365 +54,31 @@
   argument.
 - The candidate and base are complete models. Persistence applies the differences from the base
   to the candidate on top of the live state. Omitting an acquired optional key requests deletion,
-  not "unchanged" (see Model completeness). Never strip keys or upload a partial document; the runner
-  rejects a candidate that lost a key the acquired model had, except for supported explicit clears.
+  not "unchanged" (see [Model completeness](reference/mutate-and-validate.md#model-completeness)).
+  Never strip keys or upload a partial document; the runner rejects a candidate that lost a key
+  the acquired model had, except for supported explicit clears.
 - Express each request as a small JavaScript module that mutates the supplied `email` SDK handle.
-  Return nothing; the runner finishes its tracked session and rejects returned replacement documents.
-  Do not edit the raw JSON object or regenerate the full model.
+  Return nothing; the runner finishes its tracked session and rejects returned replacement
+  documents. Do not edit the raw JSON object or regenerate the full model.
 - Run the module with `run-sdk.mjs --input <model> --script <module> --output <candidate>`.
   Every edit receives the full SDK handle for content, style, insertion, removal, and duplication.
   The runner rejects scripts with no SDK mutations, selector misses, and schema-invalid output.
   A model without blocks permits metadata edits only; other edits must leave at least one block.
-- Keep the acquired model untouched as the baseline for the before/after comparison and as the
-  base file the write uploads next to the candidate.
+- Keep the acquired model byte-exact as the persistence base and retain its manifest and
+  concurrency token when supplied.
 - A successful local mutation is not completion. Verify the saved model and inspect fresh
-  desktop and mobile PNG previews of the persisted email.
-
-## 1. Acquire and inspect
-
-Resolve the target `(id, type)` as described in `PROVIDER.md` and call:
-
-```text
-get_document_state(id=<id>, type=<EMAIL|TEMPLATE>)
-```
-
-Require `status=OK` and download its temporary `downloadUrl` to a local JSON file through the
-host's authorized transfer path; that file, unmodified, is the base of the write.
-
-Inspect the model:
-
-```bash
-node <skill-dir>/scripts/inspect-editor-json.mjs \
-  --input <downloaded-model.json> \
-  --output <inspection.json>
-```
-
-If inspection reports no stripes or zero blocks, acquire and download again a bounded handful of
-times. Never upload a blockless model or fall back to editing compiled HTML. If no populated model
-becomes available, report a read failure and write nothing.
-
-Use the inspection report to identify node ids, message areas, block types, visible text, links,
-image sources, effective visibility, duplicate ids, and the before-edit census. Inspect relevant
-source nodes directly when exact content matters. IDs belong to this acquisition only.
-
-`moduleId` identifies a saved library module; it is different from a structural node `id`.
-The pinned editor contract makes `moduleId` read-only. Preserve it during ordinary content
-edits. Prefer a unique node id to address an edit; a `moduleId` selector also matches descendants,
-so constrain its node kind and check cardinality before changing content.
-
-## 2. Write the change module
-
-Create one task-local file such as `<workspace>/email-<id>.changes.mjs`. Export a function and
-mutate only the supplied `email` handle:
-
-```js
-export default ({ email }) => {
-  const logos = email.select({
-    nodeKind: "block",
-    type: "image",
-    inMessageArea: "header",
-  });
-  if (logos.length !== 1) {
-    throw new Error(`Expected one header logo, found ${logos.length}.`);
-  }
-  logos[0]
-    .setSrc("https://cdn.acme.example/logo.png")
-    .setAlt("Acme")
-    .setHref("https://acme.example/");
-};
-```
-
-For document title or preheader requests, use `email.setMetadata(patch)`:
-
-```js
-export default ({ email }) => {
-  email.setMetadata({
-    title: "Spring collection",
-    preheader: {text: "Discover what's new this season", fillSpace: true},
-  });
-};
-```
-
-Read current values from the inspection's `summary.metadata`. Include only the requested fields.
-When changing only the preheader text, carry over its current `fillSpace`; use `false` if no
-preheader exists. Both preheader fields must be supplied. `PROVIDER.md` defines the limits and
-clearing semantics. This method also works when the native model has no blocks. Return nothing
-from the module, including after a chained SDK call.
-
-Use `email.one(selector)` when exactly one match is required, or `email.block(id, type)` for
-a type-checked block. `within` restricts a selector to a subtree. Use `patchSettings(patch)`
-for supported settings; unknown keys, arbitrary collection arrays and `undefined` are errors.
-Use collection methods for social/menu items, `setSpacerMode` for spacer variants, and
-`resetSettings(paths)` for confirmed resets. A line spacer's `settings.mobileBorder` overrides
-its mobile line; `null` inherits `border`. `node.describe()` exposes the pinned contract.
-Each method is atomic. The synchronous module may also receive `transaction` and group
-related changes with `transaction(email => { ... })`; failure restores the whole group.
-All ten native block types are represented. Timer reads and unrelated supported edits are
-allowed, but preview-dependent changes and unsupported structural actions fail explicitly.
-
-Rules for the module:
-
-- Use the full `EmailDocument` API declared in
-  `<bundle-root>/packages/convo-email-agent/sdk/types.d.ts`: `email.byId`, `email.select`,
-  `email.first`, `email.theme`, node mutation methods, `remove()`, `repeat(totalCount)`, and
-  `insert(componentName, {after: handle})`. `repeat` takes the final total count including the
-  original. Use the handles returned by `repeat` for subsequent edits; the original IDs are
-  replaced. Insertion also accepts `{before: handle}`.
-- Check expected selector cardinality. When a request intentionally targets every match, require at
-  least one match before iterating.
-- Encode requested strings as JavaScript string literals; never evaluate customer text as code.
-- Do not import filesystem, process, network, or shell facilities. The edit function receives only
-  `{ email }`; the module's only job is to call SDK mutation methods.
-- Preserve unrelated content, links, merge tags, visibility, and compliance content unless they
-  are explicit targets. Make structural changes as needed to fulfill the user's request.
-- Prefer a theme edit for a broad change and a node edit for an exception. Render precedence is
-  inline HTML, block, container, structure, stripe, area theme, then general theme.
-- For a text block, responsive `textAlign` accepts `left`, `center`, `right`, or `justify`; font size
-  remains part of the semantic HTML passed to `node.setContent`.
-- For a social block, edit networks through `setSocialNetwork(typeOrIndex, {url, title, alt, icon})`,
-  `addSocialNetwork({type, url, title}, {after | before})`, `removeSocialNetwork(typeOrIndex)`, and
-  `setSocialShared({style, iconSize, spaceBetweenIcons, textCustomization})`; `setLink({url, network})`
-  changes one link. The editor's rules apply: known networks never carry `icon` (only `type: "custom"`
-  does), `alt` exists only while `textCustomization` is on (enable it with `setSocialShared` first;
-  turning it off removes every alt). Links support `http(s)://`, `#`, `mailto:`, `tel:`,
-  `ftp(s)://`, `sms:`, `tg://`, and `viber:`; a different non-empty, trimmed href uses the
-  editor's `other` type. A network `type` has to be one the editor ships an icon for, `title` is at
-  most 100 UTF-16 code units and `alt` at most 500 (`String.length`, so `😀` counts as two),
-  `iconSize` is an integer 16..64 and `spaceBetweenIcons`
-  an integer 0..40 per breakpoint. Social insertion, duplication and deletion are supported;
-  `moveSocialNetwork` reorders a network through the collection API.
-- Image sources must be email-safe hosted URLs already supplied or authorized by the user or
-  reused from the reference, or a completed hosted Stripo image-job result. For a requested
-  AI change, follow `PROVIDER.md` before writing the mutation module: `edit_image` changes the
-  current asset, while `generate_image` creates a new composition. Use the current target's
-  `(id, type)`; do not create another email. Inspect the completed image, then reacquire the
-  target model before constructing the SDK mutation. For a replacement, verify that the intended
-  image still exists with the expected source. Report a concurrent deletion or source change
-  instead of overwriting it. Use `setSrc()` and `setAlt()` as needed against that fresh model.
-  For an insertion, verify the intended container/anchor still exists and insert a native image
-  block with the completed URL through the component workflow below. Retain the fresh model
-  unmodified as the persistence base. Preserve unrelated links, layout, and settings.
-  A pending or failed image job is not a replacement; keep the original asset until ready.
-  Use the returned URL directly without reuploading or recompressing it, including during
-  document-write recovery. Image tool calls happen outside the SDK module.
-  Local attachments and crops use [Upload supplied images](reference/image-upload.md).
-  WebP inputs are supported by its image tools,
-  whose completed output is PNG; WebP, AVIF, and extensionless or otherwise unknown URLs are
-  not confirmed email-safe for direct insertion. Obtain a hosted PNG/JPG/JPEG/GIF before
-  applying an image change with an unsupported source; generation is not a lossless conversion.
-
-For insertion, define and export `components` in this same change module. Each named entry is
-`{node, slots, level?}` containing a complete native reference subtree; `level` is `L1` (stripe),
-`L2` (structure), or `atoms` (block). Give slots JSON pointers relative to that subtree and use
-`inserted.slot(role)` to address a block within an inserted section. Use `slots: []` when targeting
-the inserted block directly. The SDK assigns fresh IDs. Removal and duplication need no component
-definitions. For a block type absent from the reference, construct a schema-valid native component
-in the module using the reference styles.
-
-Block `settings` accept only the keys the editor schema lists for that block type. The model's
-top-level `settings` (`general`, `stripes`, `headings`, `buttons`) holds the defaults for every
-element of a type, and some of those keys have no block-level counterpart. Check the block
-definition before setting a key on a block. When the key is absent there, change the global
-setting instead and expect the change to apply to every element of that type in the email. A key
-the block schema does not list fails validation as `unsupported property`. For example, button
-`letterSpacing` and `textTransform` exist only in `settings.buttons`; the hover effect is the boolean
-`settings.buttons.hoverButtonStyles`, with its colors in `settings.buttons.lightTheme.hoverButtonStyles`.
-
-For example, a module can define and insert a text block:
-
-```js
-const zeroPadding = {top: 0, right: 0, bottom: 0, left: 0};
-export const components = {
-  "offer-text": {
-    node: {
-      id: "offer-text-source",
-      type: "text",
-      content: "<p>Offer details.</p>",
-      settings: {
-        backgroundColor: "transparent",
-        hideElement: "no",
-        includeInOutput: "both",
-        padding: {desktop: zeroPadding, mobile: zeroPadding},
-        rightToLeftTextDirection: false,
-      },
-    },
-    slots: [],
-  },
-};
-
-export default ({email}) => {
-  email.insert("offer-text", {after: email.byId("<anchor-block-id>")});
-};
-```
-
-## 3. Run the edit
-
-```bash
-node <skill-dir>/scripts/run-sdk.mjs \
-  --input <downloaded-model.json> \
-  --script <workspace>/email-<id>.changes.mjs \
-  --output <updated-model.json> \
-  --diagnostics <diagnostics.json>
-```
-
-Require exit code 0 and confirm:
-
-- `validation.valid` is true;
-- `sdkDiagnostics.mutationCount` is positive;
-- `sdkDiagnostics.selectorMisses` is empty;
-- `inputSummary` and `outputSummary` show the intended content, block types, and counts,
-  including any additions, removals, or duplication needed for the request.
-
-If the requested result already exists, call the supplied `skip(reason)` and return. The runner
-reports `skipped` and produces no uploadable output. A setter that silently changes nothing is an
-error. Keep the acquired model, inspection, module, candidate and diagnostics as task artifacts.
-On any preparation failure, the runner removes stale output. Diagnostics distinguish JSON,
-schema, capability, loss and optional runtime stages; inspect `diagnostics.error.issues`.
-Local contract validation is against the pinned revision. `--runtime-snapshot <file>` plus
-`STRIPO_RUNTIME_PATH` explicitly requests model preflight and fails if context is missing.
-Only a successful server set followed by get confirms persistence.
-
-Repair schema errors without removing intended content. Use `diagnostics.error.errors` and
-the untouched acquired model to repair the failing fields. Do not delete a logo or another
-intended block just to obtain a valid model; report an unresolved defect if repair cannot complete.
-
-## Model completeness
-
-`set_document_state` takes the whole document; with the acquired model uploaded as the base
-the editor applies the difference between it and the candidate to the live state.
-The editor diffs the uploaded JSON against the full acquired state. Settings are canonical and complete:
-every key of `general`, `stripes`, `headings`, and `buttons`, including their `lightTheme` and
-`darkTheme` branches, is required, so a missing settings key fails validation before upload. A
-stripe's `messageArea`, `includeInOutput`, `padding`, `stripeBackgroundColor`, and
-`contentBackgroundColor`, and a column's `settings.width`, must also stay present.
-
-An omitted `stripes`, `structures`, `columns`, or `blocks` array deletes every element it held.
-Timer blocks from the acquired model cannot be deleted, including through a parent
-`remove()` or `repeat()`. Social deletion is supported in the pinned merge-service profile.
-Therefore persist the complete candidate produced from the freshly acquired model. Clear ordinary
-values by setting their cleared value. The SDK can remove an image or social link with
-`setLink({url: ""})`, or discard `link.salesforce` when `setLink` changes a button's link from
-`salesforce_mc` to another type.
-Social `networks` is replaced as a complete list: replacing or reordering networks is supported,
-including multiple custom networks. Every resulting network still has to pass schema validation
-and the editor's icon, alt, and link rules.
-
-Colors live in the theme branches. Change a light color at its `lightTheme` path, for example
-`email.setTheme("settings.stripes.lightTheme.content.linkColor", "#2457d6")`, and set a dark-mode
-override at the matching `darkTheme` path, where `null` means no override. Switch the button hover
-effect with the boolean `settings.buttons.hoverButtonStyles`.
-
-For an intentional reset, write `undefined` or `null` to a nullable setting below. For example,
-`email.setTheme("settings.general.backgroundImage", undefined)` clears the email's background image,
-and `email.resetSettings(["general.customListStyles"])` disables custom list formatting.
-
-| Path | Settings that accept a reset (`null`) |
-| --- | --- |
-| `settings.general` | `backgroundImage`, `customListStyles` |
-| `settings.stripes.<area>` (`header`, `content`, `footer`, `infoArea`) | `paragraphBottomSpace` |
-| `settings.stripes.<area>` (`header`, `footer`) | `backgroundImage` |
-| `settings.headings.h1` through `settings.headings.h6` | `fontWeight`, `paragraphBottomSpace`, `fontSize.desktop`, `fontSize.mobile`, `lineHeight.desktop`, `lineHeight.mobile`, `textAlign.mobile` |
-| `settings.<section>.darkTheme` | every color |
-
-A nested patch works the same way: `email.setTheme("settings.general", {backgroundImage: undefined})`.
-Every other setting, including light-theme colors, global `fontFamily`, `general.hideImageDownloadIcons`, and
-whole groups such as `settings.stripes.content`, has no reset; set a new value instead. Do not strip
-fields from JSON manually. The runner still reports unintended loss for accidentally absent fields,
-unknown settings, and unsupported resets.
-
-## 4. Persist by file reference
-
-Preserve native font settings and resources; there is no font normalization step. Follow the
-prepare-upload/write flow in `PROVIDER.md`:
-
-```text
-prepare_document_state_upload(id=<id>, type=<type>)   # ticket for <updated-model.json>
-prepare_document_state_upload(id=<id>, type=<type>)   # ticket for <downloaded-model.json>
-set_document_state(id=<id>, type=<type>, uploadId=<first ticket>, baseUploadId=<second ticket>)
-```
-
-Between the calls, upload `<updated-model.json>` to the first `uploadUrl` and the untouched
-`<downloaded-model.json>` to the second, both through the consuming agent's authorized transfer
-mechanism. Never pass a model inline and never swap the two ids. With the base the editor writes
-only this edit on top of the live state and concurrent edits by others survive. Tickets are
-single-use and there is no hash or idempotency argument: keep the read-to-write gap short, and
-after an uncertain write read the state before retrying with fresh tickets. Follow the `status`
-contract in `PROVIDER.md`: a `MERGE_BROKEN` answer names the rejected fields in
-`details.errors[].path` (a dot path into `<updated-model.json>`); fix them in the change module,
-rerun the runner, and persist with two fresh tickets. `UPLOAD_NOT_FOUND` names the empty ticket in
-`missingUploadId`: a missing base has spent nothing, finish that upload and call again; a missing
-candidate has spent the base, so upload both files to fresh tickets. `REVISION_CONFLICT` or a
-`VALIDATION_ERROR` whose message says the delta is incompatible with the current document mean
-the letter moved, not that the edit is wrong: for `REVISION_CONFLICT` repeat the same write with
-fresh tickets; otherwise acquire again, rerun the same change module on the fresh model, and
-persist with the fresh model as the base. A deleted target can instead return `WRITE_UNCONFIRMED`:
-the result is unknown, so read back first. If the changes are already present, do not write again;
-if the intended node is missing, report the conflict without recreating it or retargeting the edit.
-Otherwise rerun the change module on the fresh model and use it as the base with fresh tickets.
-If readback fails, stop and report the failure. Use this same read-first procedure after a
-protocol or transport error. Use two fresh tickets for repairs after `INVALID_STATE`, `PARSE_ERROR`
-and `SIDE_EFFECT_UNAVAILABLE` as well. Never drop `baseUploadId` to force a write. Allow at
-most one repair retry before reporting the failure with the returned `code`, paths, and messages.
-
-## 5. Verify durable state
-
-Read both surfaces after persistence:
-
-```text
-get_document_state(id=<id>, type=<type>)
-get_screenshot(id=<id>, type=<type>, mode="BOTH")
-```
-
-Download the fresh model and both PNG artifacts through the authorized transfer path. Inspect
-the model for the exact requested values, link destinations, image URLs, alt text, merge tags, and
-preserved content. Compare its depth-agnostic per-type block census with the acquisition census.
-Structural node IDs may change between reads, so compare counts and types rather than those ids.
-Saved-library `moduleId` values are persistent identities and must still match. For an explicit
-module request, verify the requested ID and content on the final saved target; translated text,
-a matching module count, or a successful write receipt alone cannot establish replacement.
-After a structural edit, compare the census with the intended additions, removals, or duplication.
-The merged result may also contain concurrent edits. Whole-target equality or an unchanged
-block count is not required: verify the requested changes and investigate unexplained loss,
-while preserving unrelated live additions and edits.
-After a metadata edit, compare the fresh model's `metadata.title`, `metadata.preheader.text`, and
-`metadata.preheader.fillSpace` with the requested values and the preserved fields. Hidden metadata
-is verified from JSON; an unchanged screenshot is expected for a metadata-only edit.
-Because the write was applied as a delta, the fresh model may also carry edits other people made
-since the acquisition; those are not defects. Report a difference outside the requested change as
-a loss only when the acquisition had it, the current state lacks it, and nothing else explains it.
-
-Open and visually inspect both PNG files with the host's image-viewing tool. Check the requested
-visible changes, text readability, image loading, spacing, alignment, clipping, and responsive
-layout. A successful preview call or download alone is not visual verification. Screenshots do
-not prove link destinations, asset URLs, or hidden values; use the saved model for those checks.
-Compare against the untouched baseline and requested changes, not only the latest candidate.
-Check that branding remains, text and buttons retain effective side spacing, and heading scale
-works on mobile. Treat unrequested losses as defects, not as successful simplification.
-
-If either preview shows a defect, write a targeted repair for the same draft, persist it, then
-obtain and inspect fresh previews of both sizes again. Do not replay a successful structural edit
-as a verification step. Stop when the requested result passes review or a concrete limitation
-prevents repair. Report "saved, visual defects unresolved" with the defect if repair cannot
-complete; do not describe that email as ready to launch.
-Screenshots show the current coediting state with export settings, including unsaved changes; the
-fresh model read is the durable check. `PARTIAL` or unavailable previews mean "saved, visual
-verification incomplete" when the model checks pass.
-
-Do not rerun the change module for verification. Report full success only when persistence, model
-checks, and the required visual checks pass. If PNG generation, download, or inspection is unavailable or
-fails after a bounded retry, report "saved, visual verification incomplete" when persistence and
-model checks passed. Keep the same id for a later preview retry. Do not substitute an HTML
-export, inspect HTML as the visual check, or repeat a successful write to obtain a preview.
+  desktop and mobile PNG previews, except when `PROVIDER.md` defines preheader-only checks.
+  Then run the provider's final presentation step, when `PROVIDER.md` defines one.
 
 ## Boundaries
 
 - Do not create, clone, translate, send, schedule, activate, or delete an email.
-- Do not change name, project, or folder metadata; the Stripo MCP has no metadata write tool.
-  Report a requested change to those external fields as unsupported. Native document title and
-  preheader are supported through `email.setMetadata()` and `set_document_state`.
+- Change only requested native metadata. Preserve external sending and organization metadata;
+  `PROVIDER.md` defines supported fields and any separate route for explicitly requested changes.
+  Preheaders use `metadata.preheader`. A requested change is complete only when its saved value
+  is verified, including a fresh read confirming an already-correct value.
 - Do not use compiled HTML as the editable source of truth.
 - Do not mutate a message that has no native editor model.
-- Generate and edit requested visuals through the Stripo image workflow in `PROVIDER.md`.
-  Inspection-only requests must not start image jobs. Keep image-provider calls outside the SDK.
+- Use the image operations in `PROVIDER.md`; keep them outside the SDK module.
 - Do not infer MCP endpoints, credentials, or authentication.
 - Use `email-from-reference` for a new persisted email.
Full technical diff · 3 changed fields

changed /description

BEFORE
"Edit an existing Stripo email or template through its native JSON model. Use for document title and preheader, copy, link, image-source, AI image generation/editing, theme, style, and block additions, removals, or duplication when the email or template has an editor document state. Uses Stripo MCP image tools for requested visuals and a generated SDK mutation module, validated file-based execution, and durable read-back for model changes. Use email-from-reference for a new email or full rebuild. Do not use for raw-HTML editing, sending, scheduling, changes to name/project/folder, or content without a native model."
AFTER
"Edit an existing Stripo email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block additions, removals, or duplication. Uses validated SDK mutations, base/target merging and durable readback. Use email-from-reference for new emails/full rebuilds. Not for raw HTML, translation, sending/scheduling, external metadata (subject, sender, name, project or folder), or content without a native model."

changed /included_files

BEFORE
[
  {
    "relative_path": "HOST.md",
    "size_in_bytes": 6991
  },
  {
    "relative_path": "PROVIDER.md",
    "size_in_bytes": 19378
  },
  {
    "relative_path": "agents/claude.yaml",
    "size_in_bytes": 286
  },
  {
    "relative_path": "agents/openai.yaml",
    "size_in_bytes": 640
  },
  {
    "relative_path": "reference/image-upload.md",
    "size_in_bytes": 7606
  },
  {
    "relative_path": "scripts/inspect-editor-json.mjs",
    "size_in_bytes": 14694
  },
  {
    "relative_path": "scripts/run-sdk.mjs",
    "size_in_bytes": 22586
  }
]
AFTER
[
  {
    "relative_path": "HOST.md",
    "size_in_bytes": 8320
  },
  {
    "relative_path": "PROVIDER.md",
    "size_in_bytes": 2596
  },
  {
    "relative_path": "agents/claude.yaml",
    "size_in_bytes": 286
  },
  {
    "relative_path": "agents/openai.yaml",
    "size_in_bytes": 328
  },
  {
    "relative_path": "reference/acquire-and-inspect.md",
    "size_in_bytes": 4657
  },
  {
    "relative_path": "reference/color-consistency.md",
    "size_in_bytes": 4818
  },
  {
    "relative_path": "reference/editor-limitations.md",
    "size_in_bytes": 4570
  },
  {
    "relative_path": "reference/image-upload.md",
    "size_in_bytes": 4773
  },
  {
    "relative_path": "reference/mutate-and-validate.md",
    "size_in_bytes": 12582
  },
  {
    "relative_path": "reference/mutation-options.md",
    "size_in_bytes": 7272
  },
  {
    "relative_path": "reference/parallel-work.md",
    "size_in_bytes": 3824
  },
  {
    "relative_path": "reference/persist-and-verify.md",
    "size_in_bytes": 8401
  },
  {
    "relative_path": "reference/stripo-acquisition.md",
    "size_in_bytes": 6220
  },
  {
    "relative_path": "reference/stripo-creation.md",
    "size_in_bytes": 2871
  },
  {
    "relative_path": "reference/stripo-errors.md",
    "size_in_bytes": 6665
  },
  {
    "relative_path": "reference/stripo-images.md",
    "size_in_bytes": 11520
  },
  {
    "relative_path": "reference/stripo-persistence.md",
    "size_in_bytes": 8343
  },
  {
    "relative_path": "scripts/inspect-editor-json.mjs",
    "size_in_bytes": 18348
  },
  {
    "relative_path": "scripts/run-sdk.mjs",
    "size_in_bytes": 24327
  }
]

changed /skill_md_contents

BEFORE
"---\nname: email-model-editor\ndescription: \"Edit an existing Stripo email or template through its native JSON model. Use for document title and preheader, copy, link, image-source, AI image generation/editing, theme, style, and block additions, removals, or duplication when the email or template has an editor document state. Uses Stripo MCP image tools for requested visuals and a generated SDK mutation module, validated file-based execution, and durable read-back for model changes. Use email-from-reference for a new email or full rebuild. Do not use for raw-HTML editing, sending, scheduling, changes to name/project/folder, or content without a native model.\"\n---\n\n# Email Model Editor\n\nHost paths: in Claude Code `<skill-dir>` is `${CLAUDE_SKILL_DIR}` and `<bundle-root>` is\n`${CLAUDE_PLUGIN_ROOT}`. In Codex `<skill-dir>` is the directory of this `SKILL.md`. In both\nhosts `<bundle-root>` is two levels above `<skill-dir>`. Read `HOST.md` beside this file for\nthe working directory and file-transfer commands before the first MCP call.\n\nEdit an existing Stripo email or template through its JSON model without flattening it to HTML.\n`PROVIDER.md` beside this file defines the Stripo MCP contract: reference and target resolution,\nmodel acquisition, image jobs, persistence, and verification. Follow it for every MCP\nstep below; `<bundle-root>/mcp-tools.json` lists the tool names. The bundle contains no MCP\nendpoint or credential; use the Stripo MCP server and authorized file-transfer mechanism\nconfigured by the consuming agent.\nIf the host installs `HOST.md` beside this file, read it for concrete file-transfer commands.\nThe image workflow is supplied by `PROVIDER.md` and does not require a host image adapter.\n\nFor an attached or Ctrl+V-pasted replacement image, follow [Upload supplied images](reference/image-upload.md)\nbefore editing the model. The host must supply the original bytes as a local file. Host that\nfile with `prepare_image_upload`, the authorized HTTP transfer, and `upload_image`; use only\nthe verified final `data.url`. Reacquire the target and check its current image source before\nreplacement. Keep the original asset if hosting fails; do not regenerate a supplied image.\n\nWhen repairing a draft created from a reference, retain its chosen Exact, Tailored, or Creative\nregime and original PNG baseline. For Exact, preserve source choices, reuse existing assets or\ncrops before considering AI generation, and compare the repaired PNGs with the original source.\nDo not reset the regime or adapt the branding during the repair.\n\nValidation runs the editor's own Document State rules bundled into the SDK. No schema download\nor initialization is required. `bundle.json.editorValidator` records the editor revision.\n`finish()` validates edits against the acquired input; create mode validates a new document.\nThe service also validates its deployed revision and applies the write to its live state;\nlocal success does not establish persistence.\n\n`<skill-dir>` is this installed skill directory. `<bundle-root>` is the directory containing the\ninstalled `skills/` and `packages/` directories.\n\n## Invariants\n\n- The model is a file. Never paste the complete model into the conversation or pass it as an MCP\n  argument.\n- The candidate and base are complete models. Persistence applies the differences from the base\n  to the candidate on top of the live state. Omitting an acquired optional key requests deletion,\n  not \"unchanged\" (see Model completeness). Never strip keys or upload a partial document; the runner\n  rejects a candidate that lost a key the acquired model had, except for supported explicit clears.\n- Express each request as a small JavaScript module that mutates the supplied `email` SDK handle.\n  Return nothing; the runner finishes its tracked session and rejects returned replacement documents.\n  Do not edit the raw JSON object or regenerate the full model.\n- Run the module with `run-sdk.mjs --input <model> --script <module> --output <candidate>`.\n  Every edit receives the full SDK handle for content, style, insertion, removal, and duplication.\n  The runner rejects scripts with no SDK mutations, selector misses, and schema-invalid output.\n  A model without blocks permits metadata edits only; other edits must leave at least one block.\n- Keep the acquired model untouched as the baseline for the before/after comparison and as the\n  base file the write uploads next to the candidate.\n- A successful local mutation is not completion. Verify the saved model and inspect fresh\n  desktop and mobile PNG previews of the persisted email.\n\n## 1. Acquire and inspect\n\nResolve the target `(id, type)` as described in `PROVIDER.md` and call:\n\n```text\nget_document_state(id=<id>, type=<EMAIL|TEMPLATE>)\n```\n\nRequire `status=OK` and download its temporary `downloadUrl` to a local JSON file through the\nhost's authorized transfer path; that file, unmodified, is the base of the write.\n\nInspect the model:\n\n```bash\nnode <skill-dir>/scripts/inspect-editor-json.mjs \\\n  --input <downloaded-model.json> \\\n  --output <inspection.json>\n```\n\nIf inspection reports no stripes or zero blocks, acquire and download again a bounded handful of\ntimes. Never upload a blockless model or fall back to editing compiled HTML. If no populated model\nbecomes available, report a read failure and write nothing.\n\nUse the inspection report to identify node ids, message areas, block types, visible text, links,\nimage sources, effective visibility, duplicate ids, and the before-edit census. Inspect relevant\nsource nodes directly when exact content matters. IDs belong to this acquisition only.\n\n`moduleId` identifies a saved library module; it is different from a structural node `id`.\nThe pinned editor contract makes `moduleId` read-only. Preserve it during ordinary content\nedits. Prefer a unique node id to address an edit; a `moduleId` selector also matches descendants,\nso constrain its node kind and check cardinality before changing content.\n\n## 2. Write the change module\n\nCreate one task-local file such as `<workspace>/email-<id>.changes.mjs`. Export a function and\nmutate only the supplied `email` handle:\n\n```js\nexport default ({ email }) => {\n  const logos = email.select({\n    nodeKind: \"block\",\n    type: \"image\",\n    inMessageArea: \"header\",\n  });\n  if (logos.length !== 1) {\n    throw new Error(`Expected one header logo, found ${logos.length}.`);\n  }\n  logos[0]\n    .setSrc(\"https://cdn.acme.example/logo.png\")\n    .setAlt(\"Acme\")\n    .setHref(\"https://acme.example/\");\n};\n```\n\nFor document title or preheader requests, use `email.setMetadata(patch)`:\n\n```js\nexport default ({ email }) => {\n  email.setMetadata({\n    title: \"Spring collection\",\n    preheader: {text: \"Discover what's new this season\", fillSpace: true},\n  });\n};\n```\n\nRead current values from the inspection's `summary.metadata`. Include only the requested fields.\nWhen changing only the preheader text, carry over its current `fillSpace`; use `false` if no\npreheader exists. Both preheader fields must be supplied. `PROVIDER.md` defines the limits and\nclearing semantics. This method also works when the native model has no blocks. Return nothing\nfrom the module, including after a chained SDK call.\n\nUse `email.one(selector)` when exactly one match is required, or `email.block(id, type)` for\na type-checked block. `within` restricts a selector to a subtree. Use `patchSettings(patch)`\nfor supported settings; unknown keys, arbitrary collection arrays and `undefined` are errors.\nUse collection methods for social/menu items, `setSpacerMode` for spacer variants, and\n`resetSettings(paths)` for confirmed resets. A line spacer's `settings.mobileBorder` overrides\nits mobile line; `null` inherits `border`. `node.describe()` exposes the pinned contract.\nEach method is atomic. The synchronous module may also receive `transaction` and group\nrelated changes with `transaction(email => { ... })`; failure restores the whole group.\nAll ten native block types are represented. Timer reads and unrelated supported edits are\nallowed, but preview-dependent changes and unsupported structural actions fail explicitly.\n\nRules for the module:\n\n- Use the full `EmailDocument` API declared in\n  `<bundle-root>/packages/convo-email-agent/sdk/types.d.ts`: `email.byId`, `email.select`,\n  `email.first`, `email.theme`, node mutation methods, `remove()`, `repeat(totalCount)`, and\n  `insert(componentName, {after: handle})`. `repeat` takes the final total count including the\n  original. Use the handles returned by `repeat` for subsequent edits; the original IDs are\n  replaced. Insertion also accepts `{before: handle}`.\n- Check expected selector cardinality. When a request intentionally targets every match, require at\n  least one match before iterating.\n- Encode requested strings as JavaScript string literals; never evaluate customer text as code.\n- Do not import filesystem, process, network, or shell facilities. The edit function receives only\n  `{ email }`; the module's only job is to call SDK mutation methods.\n- Preserve unrelated content, links, merge tags, visibility, and compliance content unless they\n  are explicit targets. Make structural changes as needed to fulfill the user's request.\n- Prefer a theme edit for a broad change and a node edit for an exception. Render precedence is\n  inline HTML, block, container, structure, stripe, area theme, then general theme.\n- For a text block, responsive `textAlign` accepts `left`, `center`, `right`, or `justify`; font size\n  remains part of the semantic HTML passed to `node.setContent`.\n- For a social block, edit networks through `setSocialNetwork(typeOrIndex, {url, title, alt, icon})`,\n  `addSocialNetwork({type, url, title}, {after | before})`, `removeSocialNetwork(typeOrIndex)`, and\n  `setSocialShared({style, iconSize, spaceBetweenIcons, textCustomization})`; `setLink({url, network})`\n  changes one link. The editor's rules apply: known networks never carry `icon` (only `type: \"custom\"`\n  does), `alt` exists only while `textCustomization` is on (enable it with `setSocialShared` first;\n  turning it off removes every alt). Links support `http(s)://`, `#`, `mailto:`, `tel:`,\n  `ftp(s)://`, `sms:`, `tg://`, and `viber:`; a different non-empty, trimmed href uses the\n  editor's `other` type. A network `type` has to be one the editor ships an icon for, `title` is at\n  most 100 UTF-16 code units and `alt` at most 500 (`String.length`, so `😀` counts as two),\n  `iconSize` is an integer 16..64 and `spaceBetweenIcons`\n  an integer 0..40 per breakpoint. Social insertion, duplication and deletion are supported;\n  `moveSocialNetwork` reorders a network through the collection API.\n- Image sources must be email-safe hosted URLs already supplied or authorized by the user or\n  reused from the reference, or a completed hosted Stripo image-job result. For a requested\n  AI change, follow `PROVIDER.md` before writing the mutation module: `edit_image` changes the\n  current asset, while `generate_image` creates a new composition. Use the current target's\n  `(id, type)`; do not create another email. Inspect the completed image, then reacquire the\n  target model before constructing the SDK mutation. For a replacement, verify that the intended\n  image still exists with the expected source. Report a concurrent deletion or source change\n  instead of overwriting it. Use `setSrc()` and `setAlt()` as needed against that fresh model.\n  For an insertion, verify the intended container/anchor still exists and insert a native image\n  block with the completed URL through the component workflow below. Retain the fresh model\n  unmodified as the persistence base. Preserve unrelated links, layout, and settings.\n  A pending or failed image job is not a replacement; keep the original asset until ready.\n  Use the returned URL directly without reuploading or recompressing it, including during\n  document-write recovery. Image tool calls happen outside the SDK module.\n  Local attachments and crops use [Upload supplied images](reference/image-upload.md).\n  WebP inputs are supported by its image tools,\n  whose completed output is PNG; WebP, AVIF, and extensionless or otherwise unknown URLs are\n  not confirmed email-safe for direct insertion. Obtain a hosted PNG/JPG/JPEG/GIF before\n  applying an image change with an unsupported source; generation is not a lossless conversion.\n\nFor insertion, define and export `components` in this same change module. Each named entry is\n`{node, slots, level?}` containing a complete native reference subtree; `level` is `L1` (stripe),\n`L2` (structure), or `atoms` (block). Give slots JSON pointers relative to that subtree and use\n`inserted.slot(role)` to address a block within an inserted section. Use `slots: []` when targeting\nthe inserted block directly. The SDK assigns fresh IDs. Removal and duplication need no component\ndefinitions. For a block type absent from the reference, construct a schema-valid native component\nin the module using the reference styles.\n\nBlock `settings` accept only the keys the editor schema lists for that block type. The model's\ntop-level `settings` (`general`, `stripes`, `headings`, `buttons`) holds the defaults for every\nelement of a type, and some of those keys have no block-level counterpart. Check the block\ndefinition before setting a key on a block. When the key is absent there, change the global\nsetting instead and expect the change to apply to every element of that type in the email. A key\nthe block schema does not list fails validation as `unsupported property`. For example, button\n`letterSpacing` and `textTransform` exist only in `settings.buttons`; the hover effect is the boolean\n`settings.buttons.hoverButtonStyles`, with its colors in `settings.buttons.lightTheme.hoverButtonStyles`.\n\nFor example, a module can define and insert a text block:\n\n```js\nconst zeroPadding = {top: 0, right: 0, bottom: 0, left: 0};\nexport const components = {\n  \"offer-text\": {\n    node: {\n      id: \"offer-text-source\",\n      type: \"text\",\n      content: \"<p>Offer details.</p>\",\n      settings: {\n        backgroundColor: \"transparent\",\n        hideElement: \"no\",\n        includeInOutput: \"both\",\n        padding: {desktop: zeroPadding, mobile: zeroPadding},\n        rightToLeftTextDirection: false,\n      },\n    },\n    slots: [],\n  },\n};\n\nexport default ({email}) => {\n  email.insert(\"offer-text\", {after: email.byId(\"<anchor-block-id>\")});\n};\n```\n\n## 3. Run the edit\n\n```bash\nnode <skill-dir>/scripts/run-sdk.mjs \\\n  --input <downloaded-model.json> \\\n  --script <workspace>/email-<id>.changes.mjs \\\n  --output <updated-model.json> \\\n  --diagnostics <diagnostics.json>\n```\n\nRequire exit code 0 and confirm:\n\n- `validation.valid` is true;\n- `sdkDiagnostics.mutationCount` is positive;\n- `sdkDiagnostics.selectorMisses` is empty;\n- `inputSummary` and `outputSummary` show the intended content, block types, and counts,\n  including any additions, removals, or duplication needed for the request.\n\nIf the requested result already exists, call the supplied `skip(reason)` and return. The runner\nreports `skipped` and produces no uploadable output. A setter that silently changes nothing is an\nerror. Keep the acquired model, inspection, module, candidate and diagnostics as task artifacts.\nOn any preparation failure, the runner removes stale output. Diagnostics distinguish JSON,\nschema, capability, loss and optional runtime stages; inspect `diagnostics.error.issues`.\nLocal contract validation is against the pinned revision. `--runtime-snapshot <file>` plus\n`STRIPO_RUNTIME_PATH` explicitly requests model preflight and fails if context is missing.\nOnly a successful server set followed by get confirms persistence.\n\nRepair schema errors without removing intended content. Use `diagnostics.error.errors` and\nthe untouched acquired model to repair the failing fields. Do not delete a logo or another\nintended block just to obtain a valid model; report an unresolved defect if repair cannot complete.\n\n## Model completeness\n\n`set_document_state` takes the whole document; with the acquired model uploaded as the base\nthe editor applies the difference between it and the candidate to the live state.\nThe editor diffs the uploaded JSON against the full acquired state. Settings are canonical and complete:\nevery key of `general`, `stripes`, `headings`, and `buttons`, including their `lightTheme` and\n`darkTheme` branches, is required, so a missing settings key fails validation before upload. A\nstripe's `messageArea`, `includeInOutput`, `padding`, `stripeBackgroundColor`, and\n`contentBackgroundColor`, and a column's `settings.width`, must also stay present.\n\nAn omitted `stripes`, `structures`, `columns`, or `blocks` array deletes every element it held.\nTimer blocks from the acquired model cannot be deleted, including through a parent\n`remove()` or `repeat()`. Social deletion is supported in the pinned merge-service profile.\nTherefore persist the complete candidate produced from the freshly acquired model. Clear ordinary\nvalues by setting their cleared value. The SDK can remove an image or social link with\n`setLink({url: \"\"})`, or discard `link.salesforce` when `setLink` changes a button's link from\n`salesforce_mc` to another type.\nSocial `networks` is replaced as a complete list: replacing or reordering networks is supported,\nincluding multiple custom networks. Every resulting network still has to pass schema validation\nand the editor's icon, alt, and link rules.\n\nColors live in the theme branches. Change a light color at its `lightTheme` path, for example\n`email.setTheme(\"settings.stripes.lightTheme.content.linkColor\", \"#2457d6\")`, and set a dark-mode\noverride at the matching `darkTheme` path, where `null` means no override. Switch the button hover\neffect with the boolean `settings.buttons.hoverButtonStyles`.\n\nFor an intentional reset, write `undefined` or `null` to a nullable setting below. For example,\n`email.setTheme(\"settings.general.backgroundImage\", undefined)` clears the email's background image,\nand `email.resetSettings([\"general.customListStyles\"])` disables custom list formatting.\n\n| Path | Settings that accept a reset (`null`) |\n| --- | --- |\n| `settings.general` | `backgroundImage`, `customListStyles` |\n| `settings.stripes.<area>` (`header`, `content`, `footer`, `infoArea`) | `paragraphBottomSpace` |\n| `settings.stripes.<area>` (`header`, `footer`) | `backgroundImage` |\n| `settings.headings.h1` through `settings.headings.h6` | `fontWeight`, `paragraphBottomSpace`, `fontSize.desktop`, `fontSize.mobile`, `lineHeight.desktop`, `lineHeight.mobile`, `textAlign.mobile` |\n| `settings.<section>.darkTheme` | every color |\n\nA nested patch works the same way: `email.setTheme(\"settings.general\", {backgroundImage: undefined})`.\nEvery other setting, including light-theme colors, global `fontFamily`, `general.hideImageDownloadIcons`, and\nwhole groups such as `settings.stripes.content`, has no reset; set a new value instead. Do not strip\nfields from JSON manually. The runner still reports unintended loss for accidentally absent fields,\nunknown settings, and unsupported resets.\n\n## 4. Persist by file reference\n\nPreserve native font settings and resources; there is no font normalization step. Follow the\nprepare-upload/write flow in `PROVIDER.md`:\n\n```text\nprepare_document_state_upload(id=<id>, type=<type>)   # ticket for <updated-model.json>\nprepare_document_state_upload(id=<id>, type=<type>)   # ticket for <downloaded-model.json>\nset_document_state(id=<id>, type=<type>, uploadId=<first ticket>, baseUploadId=<second ticket>)\n```\n\nBetween the calls, upload `<updated-model.json>` to the first `uploadUrl` and the untouched\n`<downloaded-model.json>` to the second, both through the consuming agent's authorized transfer\nmechanism. Never pass a model inline and never swap the two ids. With the base the editor writes\nonly this edit on top of the live state and concurrent edits by others survive. Tickets are\nsingle-use and there is no hash or idempotency argument: keep the read-to-write gap short, and\nafter an uncertain write read the state before retrying with fresh tickets. Follow the `status`\ncontract in `PROVIDER.md`: a `MERGE_BROKEN` answer names the rejected fields in\n`details.errors[].path` (a dot path into `<updated-model.json>`); fix them in the change module,\nrerun the runner, and persist with two fresh tickets. `UPLOAD_NOT_FOUND` names the empty ticket in\n`missingUploadId`: a missing base has spent nothing, finish that upload and call again; a missing\ncandidate has spent the base, so upload both files to fresh tickets. `REVISION_CONFLICT` or a\n`VALIDATION_ERROR` whose message says the delta is incompatible with the current document mean\nthe letter moved, not that the edit is wrong: for `REVISION_CONFLICT` repeat the same write with\nfresh tickets; otherwise acquire again, rerun the same change module on the fresh model, and\npersist with the fresh model as the base. A deleted target can instead return `WRITE_UNCONFIRMED`:\nthe result is unknown, so read back first. If the changes are already present, do not write again;\nif the intended node is missing, report the conflict without recreating it or retargeting the edit.\nOtherwise rerun the change module on the fresh model and use it as the base with fresh tickets.\nIf readback fails, stop and report the failure. Use this same read-first procedure after a\nprotocol or transport error. Use two fresh tickets for repairs after `INVALID_STATE`, `PARSE_ERROR`\nand `SIDE_EFFECT_UNAVAILABLE` as well. Never drop `baseUploadId` to force a write. Allow at\nmost one repair retry before reporting the failure with the returned `code`, paths, and messages.\n\n## 5. Verify durable state\n\nRead both surfaces after persistence:\n\n```text\nget_document_state(id=<id>, type=<type>)\nget_screenshot(id=<id>, type=<type>, mode=\"BOTH\")\n```\n\nDownload the fresh model and both PNG artifacts through the authorized transfer path. Inspect\nthe model for the exact requested values, link destinations, image URLs, alt text, merge tags, and\npreserved content. Compare its depth-agnostic per-type block census with the acquisition census.\nStructural node IDs may change between reads, so compare counts and types rather than those ids.\nSaved-library `moduleId` values are persistent identities and must still match. For an explicit\nmodule request, verify the requested ID and content on the final saved target; translated text,\na matching module count, or a successful write receipt alone cannot establish replacement.\nAfter a structural edit, compare the census with the intended additions, removals, or duplication.\nThe merged result may also contain concurrent edits. Whole-target equality or an unchanged\nblock count is not required: verify the requested changes and investigate unexplained loss,\nwhile preserving unrelated live additions and edits.\nAfter a metadata edit, compare the fresh model's `metadata.title`, `metadata.preheader.text`, and\n`metadata.preheader.fillSpace` with the requested values and the preserved fields. Hidden metadata\nis verified from JSON; an unchanged screenshot is expected for a metadata-only edit.\nBecause the write was applied as a delta, the fresh model may also carry edits other people made\nsince the acquisition; those are not defects. Report a difference outside the requested change as\na loss only when the acquisition had it, the current state lacks it, and nothing else explains it.\n\nOpen and visually inspect both PNG files with the host's image-viewing tool. Check the requested\nvisible changes, text readability, image loading, spacing, alignment, clipping, and responsive\nlayout. A successful preview call or download alone is not visual verification. Screenshots do\nnot prove link destinations, asset URLs, or hidden values; use the saved model for those checks.\nCompare against the untouched baseline and requested changes, not only the latest candidate.\nCheck that branding remains, text and buttons retain effective side spacing, and heading scale\nworks on mobile. Treat unrequested losses as defects, not as successful simplification.\n\nIf either preview shows a defect, write a targeted repair for the same draft, persist it, then\nobtain and inspect fresh previews of both sizes again. Do not replay a successful structural edit\nas a verification step. Stop when the requested result passes review or a concrete limitation\nprevents repair. Report \"saved, visual defects unresolved\" with the defect if repair cannot\ncomplete; do not describe that email as ready to launch.\nScreenshots show the current coediting state with export settings, including unsaved changes; the\nfresh model read is the durable check. `PARTIAL` or unavailable previews mean \"saved, visual\nverification incomplete\" when the model checks pass.\n\nDo not rerun the change module for verification. Report full success only when persistence, model\nchecks, and the required visual checks pass. If PNG generation, download, or inspection is unavailable or\nfails after a bounded retry, report \"saved, visual verification incomplete\" when persistence and\nmodel checks passed. Keep the same id for a later preview retry. Do not substitute an HTML\nexport, inspect HTML as the visual check, or repeat a successful write to obtain a preview.\n\n## Boundaries\n\n- Do not create, clone, translate, send, schedule, activate, or delete an email.\n- Do not change name, project, or folder metadata; the Stripo MCP has no metadata write tool.\n  Report a requested change to those external fields as unsupported. Native document title and\n  preheader are supported through `email.setMetadata()` and `set_document_state`.\n- Do not use compiled HTML as the editable source of truth.\n- Do not mutate a message that has no native editor model.\n- Generate and edit requested visuals through the Stripo image workflow in `PROVIDER.md`.\n  Inspection-only requests must not start image jobs. Keep image-provider calls outside the SDK.\n- Do not infer MCP endpoints, credentials, or authentication.\n- Use `email-from-reference` for a new persisted email.\n"
AFTER
"---\nname: email-model-editor\ndescription: \"Edit an existing Stripo email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block additions, removals, or duplication. Uses validated SDK mutations, base/target merging and durable readback. Use email-from-reference for new emails/full rebuilds. Not for raw HTML, translation, sending/scheduling, external metadata (subject, sender, name, project or folder), or content without a native model.\"\n---\n\n# Email Model Editor\n\nEdit content, styles, and structure of an existing email without flattening the native model to\nHTML. Use `email-from-reference` for a new email or full rebuild.\n\nReuse instructions already present, including a complete injected `SKILL.md` and shared topics\nacross both email skills in the same provider/bundle. Reread missing, truncated or changed\ndocuments and reload on provider/version changes. All checks and remote-state freshness apply.\n\nRead [PROVIDER.md](PROVIDER.md) once before tool discovery for target resolution, metadata,\ntransfers, images, persistence and verification. If installed, read `HOST.md` for bundle paths,\ntransfer/attachment bindings, image viewing, permissions and delegated image operations.\n`mcp-tools.json` maps operation roles to advertised names; discover the selected server's actual\nschemas, not those logical keys. The bundle contains no endpoint or credentials.\n\nUse one agent by default and batch independent tool calls. For a newly acquired edit task, read\nthe acquisition, mutation, persistence and [parallel work](reference/parallel-work.md) guides plus\nthe provider's acquisition and persistence topics not already loaded, each in its own tool call.\nThe parallel guide limits delegation to large independent subtasks or separate reviews. For a\nrepair continuing creation, use the continuation below. Load conditional topics on demand,\nnever the full tool/resource catalog.\n\n| Stage | Common guides read together; conditional topics when needed |\n| --- | --- |\n| Acquire | [Download and inspect the target](reference/acquire-and-inspect.md) before editing. |\n| Mutate | [SDK module, run, validation, completeness](reference/mutate-and-validate.md) before constructing the change. |\n| Conditional mutation | [Metadata, social, insertion, global block settings, theme resets and fonts](reference/mutation-options.md) when the request includes these operations. |\n| Text or colors | [Color consistency](reference/color-consistency.md) before changing text markup, colors, backgrounds, or inserting text. |\n| Editor limits | [Editor limitations](reference/editor-limitations.md) before changing text color, line height, or layout. |\n| Persist | [File upload, recovery, durable and visual checks, final presentation](reference/persist-and-verify.md) before writing. |\n| Attached or Ctrl+V image | [Upload supplied images](reference/image-upload.md) before hosting original bytes. |\n\nFor an attached or Ctrl+V-pasted replacement image, the host must supply the original bytes as\na local file. Host it before editing the model and use only the verified final `data.url`. Reacquire the\ntarget and check its current image source before replacement. Keep\nthe original asset if hosting fails; do not regenerate a supplied image.\n\n## Continue after creation\n\nFor a repair of the same saved draft, use [the continuation](reference/mutate-and-validate.md#continue-after-creation).\nCarry forward the latest readback and loaded contracts; keep all edit invariants and verification below.\n\n`<skill-dir>` is this installed skill directory. `<bundle-root>` is the directory containing the\ninstalled `skills/` and `packages/` directories.\n\n## Invariants\n\n- The model is a file. Never paste the complete model into the conversation or pass it as an MCP\n  argument.\n- The candidate and base are complete models. Persistence applies the differences from the base\n  to the candidate on top of the live state. Omitting an acquired optional key requests deletion,\n  not \"unchanged\" (see [Model completeness](reference/mutate-and-validate.md#model-completeness)).\n  Never strip keys or upload a partial document; the runner rejects a candidate that lost a key\n  the acquired model had, except for supported explicit clears.\n- Express each request as a small JavaScript module that mutates the supplied `email` SDK handle.\n  Return nothing; the runner finishes its tracked session and rejects returned replacement\n  documents. Do not edit the raw JSON object or regenerate the full model.\n- Run the module with `run-sdk.mjs --input <model> --script <module> --output <candidate>`.\n  Every edit receives the full SDK handle for content, style, insertion, removal, and duplication.\n  The runner rejects scripts with no SDK mutations, selector misses, and schema-invalid output.\n  A model without blocks permits metadata edits only; other edits must leave at least one block.\n- Keep the acquired model byte-exact as the persistence base and retain its manifest and\n  concurrency token when supplied.\n- A successful local mutation is not completion. Verify the saved model and inspect fresh\n  desktop and mobile PNG previews, except when `PROVIDER.md` defines preheader-only checks.\n  Then run the provider's final presentation step, when `PROVIDER.md` defines one.\n\n## Boundaries\n\n- Do not create, clone, translate, send, schedule, activate, or delete an email.\n- Change only requested native metadata. Preserve external sending and organization metadata;\n  `PROVIDER.md` defines supported fields and any separate route for explicitly requested changes.\n  Preheaders use `metadata.preheader`. A requested change is complete only when its saved value\n  is verified, including a fresh read confirming an already-correct value.\n- Do not use compiled HTML as the editable source of truth.\n- Do not mutate a message that has no native editor model.\n- Use the image operations in `PROVIDER.md`; keep them outside the SDK module.\n- Do not infer MCP endpoints, credentials, or authentication.\n- Use `email-from-reference` for a new persisted email.\n"
Full snapshot data
{
  "description": "Edit an existing Stripo email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block additions, removals, or duplication. Uses validated SDK mutations, base/target merging and durable readback. Use email-from-reference for new emails/full rebuilds. Not for raw HTML, translation, sending/scheduling, external metadata (subject, sender, name, project or folder), or content without a native model.",
  "included_files": [
    {
      "relative_path": "HOST.md",
      "size_in_bytes": 8320
    },
    {
      "relative_path": "PROVIDER.md",
      "size_in_bytes": 2596
    },
    {
      "relative_path": "agents/claude.yaml",
      "size_in_bytes": 286
    },
    {
      "relative_path": "agents/openai.yaml",
      "size_in_bytes": 328
    },
    {
      "relative_path": "reference/acquire-and-inspect.md",
      "size_in_bytes": 4657
    },
    {
      "relative_path": "reference/color-consistency.md",
      "size_in_bytes": 4818
    },
    {
      "relative_path": "reference/editor-limitations.md",
      "size_in_bytes": 4570
    },
    {
      "relative_path": "reference/image-upload.md",
      "size_in_bytes": 4773
    },
    {
      "relative_path": "reference/mutate-and-validate.md",
      "size_in_bytes": 12582
    },
    {
      "relative_path": "reference/mutation-options.md",
      "size_in_bytes": 7272
    },
    {
      "relative_path": "reference/parallel-work.md",
      "size_in_bytes": 3824
    },
    {
      "relative_path": "reference/persist-and-verify.md",
      "size_in_bytes": 8401
    },
    {
      "relative_path": "reference/stripo-acquisition.md",
      "size_in_bytes": 6220
    },
    {
      "relative_path": "reference/stripo-creation.md",
      "size_in_bytes": 2871
    },
    {
      "relative_path": "reference/stripo-errors.md",
      "size_in_bytes": 6665
    },
    {
      "relative_path": "reference/stripo-images.md",
      "size_in_bytes": 11520
    },
    {
      "relative_path": "reference/stripo-persistence.md",
      "size_in_bytes": 8343
    },
    {
      "relative_path": "scripts/inspect-editor-json.mjs",
      "size_in_bytes": 18348
    },
    {
      "relative_path": "scripts/run-sdk.mjs",
      "size_in_bytes": 24327
    }
  ],
  "name": "email-model-editor",
  "skill_md_contents": "---\nname: email-model-editor\ndescription: \"Edit an existing Stripo email/template through its native JSON model: document title and preheader, copy, links, images, AI image generation/editing, theme/style, and block additions, removals, or duplication. Uses validated SDK mutations, base/target merging and durable readback. Use email-from-reference for new emails/full rebuilds. Not for raw HTML, translation, sending/scheduling, external metadata (subject, sender, name, project or folder), or content without a native model.\"\n---\n\n# Email Model Editor\n\nEdit content, styles, and structure of an existing email without flattening the native model to\nHTML. Use `email-from-reference` for a new email or full rebuild.\n\nReuse instructions already present, including a complete injected `SKILL.md` and shared topics\nacross both email skills in the same provider/bundle. Reread missing, truncated or changed\ndocuments and reload on provider/version changes. All checks and remote-state freshness apply.\n\nRead [PROVIDER.md](PROVIDER.md) once before tool discovery for target resolution, metadata,\ntransfers, images, persistence and verification. If installed, read `HOST.md` for bundle paths,\ntransfer/attachment bindings, image viewing, permissions and delegated image operations.\n`mcp-tools.json` maps operation roles to advertised names; discover the selected server's actual\nschemas, not those logical keys. The bundle contains no endpoint or credentials.\n\nUse one agent by default and batch independent tool calls. For a newly acquired edit task, read\nthe acquisition, mutation, persistence and [parallel work](reference/parallel-work.md) guides plus\nthe provider's acquisition and persistence topics not already loaded, each in its own tool call.\nThe parallel guide limits delegation to large independent subtasks or separate reviews. For a\nrepair continuing creation, use the continuation below. Load conditional topics on demand,\nnever the full tool/resource catalog.\n\n| Stage | Common guides read together; conditional topics when needed |\n| --- | --- |\n| Acquire | [Download and inspect the target](reference/acquire-and-inspect.md) before editing. |\n| Mutate | [SDK module, run, validation, completeness](reference/mutate-and-validate.md) before constructing the change. |\n| Conditional mutation | [Metadata, social, insertion, global block settings, theme resets and fonts](reference/mutation-options.md) when the request includes these operations. |\n| Text or colors | [Color consistency](reference/color-consistency.md) before changing text markup, colors, backgrounds, or inserting text. |\n| Editor limits | [Editor limitations](reference/editor-limitations.md) before changing text color, line height, or layout. |\n| Persist | [File upload, recovery, durable and visual checks, final presentation](reference/persist-and-verify.md) before writing. |\n| Attached or Ctrl+V image | [Upload supplied images](reference/image-upload.md) before hosting original bytes. |\n\nFor an attached or Ctrl+V-pasted replacement image, the host must supply the original bytes as\na local file. Host it before editing the model and use only the verified final `data.url`. Reacquire the\ntarget and check its current image source before replacement. Keep\nthe original asset if hosting fails; do not regenerate a supplied image.\n\n## Continue after creation\n\nFor a repair of the same saved draft, use [the continuation](reference/mutate-and-validate.md#continue-after-creation).\nCarry forward the latest readback and loaded contracts; keep all edit invariants and verification below.\n\n`<skill-dir>` is this installed skill directory. `<bundle-root>` is the directory containing the\ninstalled `skills/` and `packages/` directories.\n\n## Invariants\n\n- The model is a file. Never paste the complete model into the conversation or pass it as an MCP\n  argument.\n- The candidate and base are complete models. Persistence applies the differences from the base\n  to the candidate on top of the live state. Omitting an acquired optional key requests deletion,\n  not \"unchanged\" (see [Model completeness](reference/mutate-and-validate.md#model-completeness)).\n  Never strip keys or upload a partial document; the runner rejects a candidate that lost a key\n  the acquired model had, except for supported explicit clears.\n- Express each request as a small JavaScript module that mutates the supplied `email` SDK handle.\n  Return nothing; the runner finishes its tracked session and rejects returned replacement\n  documents. Do not edit the raw JSON object or regenerate the full model.\n- Run the module with `run-sdk.mjs --input <model> --script <module> --output <candidate>`.\n  Every edit receives the full SDK handle for content, style, insertion, removal, and duplication.\n  The runner rejects scripts with no SDK mutations, selector misses, and schema-invalid output.\n  A model without blocks permits metadata edits only; other edits must leave at least one block.\n- Keep the acquired model byte-exact as the persistence base and retain its manifest and\n  concurrency token when supplied.\n- A successful local mutation is not completion. Verify the saved model and inspect fresh\n  desktop and mobile PNG previews, except when `PROVIDER.md` defines preheader-only checks.\n  Then run the provider's final presentation step, when `PROVIDER.md` defines one.\n\n## Boundaries\n\n- Do not create, clone, translate, send, schedule, activate, or delete an email.\n- Change only requested native metadata. Preserve external sending and organization metadata;\n  `PROVIDER.md` defines supported fields and any separate route for explicitly requested changes.\n  Preheaders use `metadata.preheader`. A requested change is complete only when its saved value\n  is verified, including a fresh read confirming an already-correct value.\n- Do not use compiled HTML as the editable source of truth.\n- Do not mutate a message that has no native editor model.\n- Use the image operations in `PROVIDER.md`; keep them outside the SDK module.\n- Do not infer MCP endpoints, credentials, or authentication.\n- Use `email-from-reference` for a new persisted email.\n"
}

SHA-256 of public snapshot: 077e7471ce3fd25610e93700dbf6e9ea999737efee7915f1514082764e7503bc