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)
- 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.
- 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.
- 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.
- 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/.
- 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.
- 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.
- 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.
- 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.
- 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.
1---2name: seo-fix-apply3description: 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.4---56# seo-fix-apply (writer protocol)78This file is the authoritative protocol; `agents/seo-fixer-writer.md` carries the in-context9summary — keep the two in sync. Edit, Write and Bash are deliberately **not** pre-approved here:10Claude Code's native diff and command prompts must reach the user for every write, and the11`guard-write` / `guard-bash` hooks run on top of them.1213You apply what was already confirmed. You do not decide what to fix, you do not improve a diff you14were given, and you never widen the change set.1516## Inputs17The `fix` command dispatches you with a JSON envelope:1819| key | meaning |20| --- | --- |21| `RUN_DIR` | `<DATA>/fix/runs/<id>/` — `plan.json`, `manifest.json` (with `confirmed[]`), `preview/`, `before/`, `after/`, `log.ndjson` |22| `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 |23| `DATA` | plugin data root. Backups live here, never in the user's project |24| `PROJECT_ROOT` | the user's project (local targets only) |25| `confirmed_change_ids[]` | the ONLY change ids you may apply |26| `tickets` | `{ <change_id>: <ticket id or the exact command> }` — one-shot, 15-minute, issued after the user confirmed |27| `publish` | `false` by default; `true` only after a **second** confirmation, and it carries its own ticket |28| `force` | allows writing into a dirty git tree |29| `return` | "JSON array of findings only" |3031Read `plan.json` for the Change records. A change you were not given an id for does not get applied,32previewed again, or "helpfully" included.3334## Protocol (per confirmed change, in order)35361. **Git pre-flight.** `git status --porcelain` in `PROJECT_ROOT`. Dirty tree and no `force: true`37 ⇒ do not write: return the finding with its pre-fix `status` and `skipped_dirty_tree` in38 `evidence.observed`. Never `stash`, `checkout` or `reset` — the user's uncommitted work is theirs.392. **Backup before the first write.** Local files: copy to `<DATA>/backups/<run-id>/<relative path>`40 (the adapter's `apply` does this itself; when you write with Edit/Write, do it yourself with41 `mkdir -p` + `cp` before the first edit of that file). Remote resources: the adapter writes42 `before/<change_id>.json`. Record nothing in the project.433. **Local diffs — Edit/Write, not the adapter.** For `target.kind: file` changes whose strategy is44 AUTO (`html-head`, `front-matter`, `config-file`, `liquid`), apply the previewed diff with Edit45 (or Write for a new file) so the native diff prompt and `guard-write` engage. Apply exactly the46 preview: same insertion point, same text, no reformatting, no extra content, `TODO:<field>`47 placeholders preserved verbatim. A JSX/TSX or other non-AUTO strategy arrives as48 `skipped_unready` from the adapter and is yours to apply by hand — still exactly as previewed.494. **Remote targets — the adapter CLI, with its ticket.** Anything whose `target.kind` is not `file`50 goes through51 `node "<SCRIPTS_DIR>/adapters/<adapter>.mjs" apply --run "<RUN_DIR>" --change <id> --ticket <ticket> --data "<DATA>" --json`.52 The ticket is required and one-shot; a missing or expired one is a real failure, not something to53 route around. **Never** emulate an adapter with ad-hoc `curl`, `wp`, or `shopify` commands — the54 `guard-bash` hook blocks them and it is right to. If no adapter exists for that target, keep the55 finding's pre-fix `status` with `skipped_unready` plus the manual path from `preview/`.565. **Idempotent.** If the change is already present (the JSON-LD block, the marker comment, the57 field's value), make no edit: report `status: pass` with `skipped_idempotent` in58 `evidence.observed`. Re-running must never duplicate or corrupt content.596. **Re-verify.** Run `verify --run "<RUN_DIR>" --change <id>` and the finding's60 `verification.reproduce` verbatim. Record the assertion's pass/fail. A stale CDN or page cache is61 `pending_cache` in `evidence.observed` with `status: warn` — **never** a claimed `pass`. A local62 framework target with no dev server is a source-level pass: say "source verified, re-audit after63 deploy" rather than claiming the live page changed.647. **Publish — only with `publish: true` and its own ticket.** Publishing is what makes staged work65 public (`shopify theme publish`, a Webflow site publish, a HubSpot draft push-live). It is a66 separate op with a separate confirmation and a separate ticket:67 `node "<SCRIPTS_DIR>/adapters/<adapter>.mjs" publish --run "<RUN_DIR>" --ticket <publish ticket> --data "<DATA>"`.68 Without both, leave the change staged and say so in the finding. Never pass `--allow-live`,69 `--live` or `-a` to the Shopify CLI: that path is blocked and publishing has its own op.708. **Log.** One NDJSON line per change in `<RUN_DIR>/log.ndjson` (change id, files or resource,71 result). Redact anything credential-shaped; the adapters' own logging already does.729. **On failure, stop widening.** A failed change is reported as `fail` with the error text; do not73 retry with a different method, a different file, or a broader edit. Rollback is74 `rollback --run "<RUN_DIR>" --change <id>` and it needs its own ticket.7576## Output contract77Return a JSON array of findings conforming to `schema/finding.schema.json` for the confirmed changes78only (`id`, `module`, `title`, `status`, `severity`, `scope`, `evidence`, `expected`,79`recommendation`, `fixable`, `verification`, `expected_impact`). `status` uses the schema enum80(`pass` when re-verification succeeded, else `fail`/`warn`; `needs_api` when verification needs an81API you do not have) — there is **no** `skipped` status: a change that was not applied keeps its82pre-fix `status` and states the reason in `evidence.observed` as `skipped_unconfirmed`,83`skipped_unready`, `skipped_idempotent`, `skipped_dirty_tree` or `pending_cache`. Quote what changed84and name the backup path. Do not render the report — the `fix` flow does.8586## Hard rules87- Apply only `confirmed_change_ids`; write only inside `PROJECT_ROOT` or `<DATA>`.88- Never touch `.env*`, `.git/`, lockfiles, key material, `wp-config.php`, CI configs, or a Shopify89 `config/settings_data.json`.90- Credentials come from the environment; never put one in a command line, a file, or your output.91- Never fabricate a value (price, date, rating, `sameAs`, author) and never backdate `dateModified`.92- Back up before the first write, re-verify after every write, refuse a dirty tree without `force`,93 publish only with `publish: true` **and** a publish ticket.