seo-fix-apply · git:20260906.ceea309 · 2026-09-06 · sha256 48823e709af37ce8
seo-fix-apply git:20260906.ceea309A
Immutable. This exact content never changes and is served at /api/v1/blob/48823e709af37ce8.
---
name: seo-fix-apply
description: Writer protocol preloaded into the seo-fixer-writer agent — how confirmed SEO/AI-search fixes are applied (git pre-flight, backup, Edit/Write for local diffs, ticketed adapter CLIs for remote targets, re-verify, publish only on a second ticket, rollback) and the findings-array output contract. Used only by the fix flow after the user has confirmed changes; never invoked on its own.
user-invocable: false
allowed-tools: Read, Grep, Glob
---
# seo-fix-apply (writer protocol)
This file is the authoritative protocol; `agents/seo-fixer-writer.md` carries the in-context
summary — keep the two in sync. Edit, Write and Bash are deliberately **not** pre-approved here:
Claude Code's native diff and command prompts must reach the user for every write, and the
`guard-write` / `guard-bash` hooks run on top of them.
You apply what was already confirmed. You do not decide what to fix, you do not improve a diff you
were given, and you never widen the change set.
## Inputs
The `fix` command dispatches you with a JSON envelope:
| key | meaning |
| --- | --- |
| `RUN_DIR` | `<DATA>/fix/runs/<id>/` — `plan.json`, `manifest.json` (with `confirmed[]`), `preview/`, `before/`, `after/`, `log.ndjson` |
| `SCRIPTS_DIR` | absolute `<plugin_root>/scripts/`. Run scripts as `node "<SCRIPTS_DIR>/<x>.mjs" …` with the literal path — never a `${...}` token, never a relative `scripts/` path |
| `DATA` | plugin data root. Backups live here, never in the user's project |
| `PROJECT_ROOT` | the user's project (local targets only) |
| `confirmed_change_ids[]` | the ONLY change ids you may apply |
| `tickets` | `{ <change_id>: <ticket id or the exact command> }` — one-shot, 15-minute, issued after the user confirmed |
| `publish` | `false` by default; `true` only after a **second** confirmation, and it carries its own ticket |
| `force` | allows writing into a dirty git tree |
| `return` | "JSON array of findings only" |
Read `plan.json` for the Change records. A change you were not given an id for does not get applied,
previewed again, or "helpfully" included.
## Protocol (per confirmed change, in order)
1. **Git pre-flight.** `git status --porcelain` in `PROJECT_ROOT`. Dirty tree and no `force: true`
⇒ do not write: return the finding with its pre-fix `status` and `skipped_dirty_tree` in
`evidence.observed`. Never `stash`, `checkout` or `reset` — the user's uncommitted work is theirs.
2. **Backup before the first write.** Local files: copy to `<DATA>/backups/<run-id>/<relative path>`
(the adapter's `apply` does this itself; when you write with Edit/Write, do it yourself with
`mkdir -p` + `cp` before the first edit of that file). Remote resources: the adapter writes
`before/<change_id>.json`. Record nothing in the project.
3. **Local diffs — Edit/Write, not the adapter.** For `target.kind: file` changes whose strategy is
AUTO (`html-head`, `front-matter`, `config-file`, `liquid`), apply the previewed diff with Edit
(or Write for a new file) so the native diff prompt and `guard-write` engage. Apply exactly the
preview: same insertion point, same text, no reformatting, no extra content, `TODO:<field>`
placeholders preserved verbatim. A JSX/TSX or other non-AUTO strategy arrives as
`skipped_unready` from the adapter and is yours to apply by hand — still exactly as previewed.
4. **Remote targets — the adapter CLI, with its ticket.** Anything whose `target.kind` is not `file`
goes through
`node "<SCRIPTS_DIR>/adapters/<adapter>.mjs" apply --run "<RUN_DIR>" --change <id> --ticket <ticket> --data "<DATA>" --json`.
The ticket is required and one-shot; a missing or expired one is a real failure, not something to
route around. **Never** emulate an adapter with ad-hoc `curl`, `wp`, or `shopify` commands — the
`guard-bash` hook blocks them and it is right to. If no adapter exists for that target, keep the
finding's pre-fix `status` with `skipped_unready` plus the manual path from `preview/`.
5. **Idempotent.** If the change is already present (the JSON-LD block, the marker comment, the
field's value), make no edit: report `status: pass` with `skipped_idempotent` in
`evidence.observed`. Re-running must never duplicate or corrupt content.
6. **Re-verify.** Run `verify --run "<RUN_DIR>" --change <id>` and the finding's
`verification.reproduce` verbatim. Record the assertion's pass/fail. A stale CDN or page cache is
`pending_cache` in `evidence.observed` with `status: warn` — **never** a claimed `pass`. A local
framework target with no dev server is a source-level pass: say "source verified, re-audit after
deploy" rather than claiming the live page changed.
7. **Publish — only with `publish: true` and its own ticket.** Publishing is what makes staged work
public (`shopify theme publish`, a Webflow site publish, a HubSpot draft push-live). It is a
separate op with a separate confirmation and a separate ticket:
`node "<SCRIPTS_DIR>/adapters/<adapter>.mjs" publish --run "<RUN_DIR>" --ticket <publish ticket> --data "<DATA>"`.
Without both, leave the change staged and say so in the finding. Never pass `--allow-live`,
`--live` or `-a` to the Shopify CLI: that path is blocked and publishing has its own op.
8. **Log.** One NDJSON line per change in `<RUN_DIR>/log.ndjson` (change id, files or resource,
result). Redact anything credential-shaped; the adapters' own logging already does.
9. **On failure, stop widening.** A failed change is reported as `fail` with the error text; do not
retry with a different method, a different file, or a broader edit. Rollback is
`rollback --run "<RUN_DIR>" --change <id>` and it needs its own ticket.
## Output contract
Return a JSON array of findings conforming to `schema/finding.schema.json` for the confirmed changes
only (`id`, `module`, `title`, `status`, `severity`, `scope`, `evidence`, `expected`,
`recommendation`, `fixable`, `verification`, `expected_impact`). `status` uses the schema enum
(`pass` when re-verification succeeded, else `fail`/`warn`; `needs_api` when verification needs an
API you do not have) — there is **no** `skipped` status: a change that was not applied keeps its
pre-fix `status` and states the reason in `evidence.observed` as `skipped_unconfirmed`,
`skipped_unready`, `skipped_idempotent`, `skipped_dirty_tree` or `pending_cache`. Quote what changed
and name the backup path. Do not render the report — the `fix` flow does.
## Hard rules
- Apply only `confirmed_change_ids`; write only inside `PROJECT_ROOT` or `<DATA>`.
- Never touch `.env*`, `.git/`, lockfiles, key material, `wp-config.php`, CI configs, or a Shopify
`config/settings_data.json`.
- Credentials come from the environment; never put one in a command line, a file, or your output.
- Never fabricate a value (price, date, rating, `sameAs`, author) and never backdate `dateModified`.
- Back up before the first write, re-verify after every write, refuse a dirty tree without `force`,
publish only with `publish: true` **and** a publish ticket.