seo-fix-apply · diff
git:20260906.fa9c08a to git:20260906.ceea309
79 added, 15 removed. Audit A to A.
---
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, adapter CLIs for remote targets, re-verify, publish only on a second confirmation) and the findings-array output contract. Used only by the fix flow after the user has confirmed changes; never invoked on its own.
+ 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)
- Created in Phase 0 so the `seo-fixer-writer` `skills:` preload resolves; Phase 3h extends it with the adapter flow (capabilities → plan → preview → ticketed apply → verify → publish → rollback). This file is the authoritative protocol and `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.
+ 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 the writer with a JSON envelope: `RUN_DIR` (fix run dir `<DATA>/fix/runs/<id>/` holding `plan.json`, `manifest.json` with `confirmed[]`, `preview/`, `before/`, `after/`, `log.ndjson`), `SCRIPTS_DIR` (absolute `<plugin_root>/scripts/`), `DATA` (plugin data root — backups live there, never in the project), `PROJECT_ROOT` (local targets only), `confirmed_change_ids[]`, `publish` (default `false`), `force` (default `false`), `return: "JSON array of findings only"`. Scripts run as `node "<SCRIPTS_DIR>/<x>.mjs" …` with the literal absolute path — never `${...}` tokens or a relative `scripts/` path. <!-- reconcile-flags-v0.2 -->
+ 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`; a dirty tree means refuse to write (`status: warn`, reason in `evidence.observed`) unless the envelope carries `force: true`. Never `stash`, `checkout`, or `reset`.
- 2. **Backup** — before the first edit of any file, copy it to `<DATA>/backups/<run-id>/<relative path>`; one backup dir per run, recorded in `manifest.json`.
- 3. **Apply exactly the preview** — Edit/Write for local diffs so the native diff prompt and the `guard-write` hook engage. AUTO strategies: `html-head`, `front-matter`, `config-file`, `liquid`; every other strategy must have been individually accepted by the user. No reformatting, no extra content; `TODO:<field>` placeholders preserved verbatim.
- 4. **Remote targets** — a change whose `target.kind` is not `file` goes through the adapter CLIs under `<SCRIPTS_DIR>/adapters/` (Phase 3: `apply` requires a ticket; `publish` is a separate op with its own ticket). Until the adapter exists, do not apply: keep the finding's pre-fix `status`, put `skipped_unready` plus the manual path from `preview` in `evidence.observed`, and never emulate an adapter with ad-hoc `curl`.
- 5. **Idempotent** — if the change is already present, make no edit and report `status: pass` with `skipped_idempotent` in `evidence.observed`. Re-runs never duplicate or corrupt content.
- 6. **Re-verify** — run the finding's `verification.reproduce` verbatim (an absolute `node "<SCRIPTS_DIR>/…" --snapshot …` command) and record the assertion's pass/fail. A stale CDN or cache is `pending_cache` in `evidence.observed` with `status: warn` — never a claimed `pass`. <!-- reconcile-flags-v0.2 -->
- 7. **Publish** — only when the envelope carries `publish: true` (the user's **second** confirmation); otherwise leave the change staged and say so.
- 8. **Log** — append one line per change to `<RUN_DIR>/log.ndjson` (change id, files, result); redact any credential-looking value.
+ 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. `status` uses the schema enum (`pass` when re-verification succeeds, else `fail`/`warn`; `needs_api` when verification needs an unavailable API) — there is no `skipped` status: a change that was not applied keeps its pre-fix `status` and states `skipped_<reason>` (`skipped_unconfirmed`, `skipped_unready`, `skipped_idempotent`) in `evidence.observed`. Quote what changed and name the backup path. Do not render the report — the `fix` flow does.
+ 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, CI configs, or anything `guard-write` protects.
- - Never fabricate values (prices, dates, ratings, `sameAs` links); 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`.
+ - 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.