mcpbeat

Review Existing Content

pulumi/review-existing-content

Review existing documentation pages selected by the daily content-review workflow. Runs the docs-review claim pipeline over whole files via synthetic diffs, applies high-confidence fixes only, and opens one auditable PR per article. Invoked by the review-existing-content workflow; not user-invocable.

9k tokens
context cost
the whole folder, loaded on every use
4
files
instructions only
0
copies elsewhere
how many repositories repackaged it
178
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/pulumi/docs --skill review-existing-content

What comes with it

12 637 bytes besides the instruction
references/pr-body-template.md
references/screenshot-verification.md
references/strategic-tiers.yaml

The instruction itself

15 sections, as written by the author

Review Existing Content

You are reviewing existing documentation pages — not a PR diff. The selection

script has already chosen today's articles; your job is to run the docs-review

machinery over each whole file and apply only the fixes you can defend with an

authoritative source. Everything judgment-level goes in the PR description for

a human, not in the diff.

You run unprivileged: the review job holds no push token, so you edit the

working tree only — never git commit, git push, or gh pr create. The

workflow's publish job validates your changes against a deterministic gate

(scripts/content-review/publish-gate.py: verdict shape, diff scope,

no_retire), derives the branch name from the queue, and opens the PR

(ready when the re-lint passes, draft only on lint failure) with the body you

edited. Changes outside the gate's scope are rejected

wholesale, so keep every edit inside the bounds each step names.

Input

Read .content-review-queue.json from the repo root (written by

scripts/content-review/select-articles.py). Shape:

{
  "generated": "2026-06-12T14:00:00+00:00",
  "count": 3,
  "halted": null,
  "traffic": { "available": true, "period": "2026-05", "pages_matched": 731 },
  "reader_signals": {
    "available": true,
    "gsc": { "available": true, "period": {"start": "2026-03-14", "end": "2026-06-11"},
             "pages_matched": 612, "median_ctr": 0.031, "max_impressions": 88012 },
    "feedback": { "available": true, "pages_matched": 214 }
  },
  "articles": [
    { "path": "content/docs/iac/concepts/stacks/_index.md",
      "url": "/docs/iac/concepts/stacks/",
      "slug": "docs-iac-concepts-stacks",
      "lane": "priority",
      "tier": 1,
      "no_retire": true,
      "monthly_visits": 12345,
      "signals": {
        "gsc": { "impressions": 15234, "ctr": 0.0205, "opportunity": 0.41,
                 "multiplier": 1.1025, "low_ctr_flag": true },
        "feedback": { "yes": 4, "no": 9, "neg_rate": 0.6923, "multiplier": 1.27 }
      },
      "last_reviewed": null,
      "score": 0.91 }
  ]
}
  • lanepriority (scored pick) or manual (workflow_dispatch override).
  • stale_claims (when present) — count of this page's volatile claims the

nightly re-verification found contradicted (see §Claims index below). A

non-zero count is why the page jumped the queue: treat the ledger markers'

entity keys and evidence as priority findings to re-check first.

  • no_retire — when true, retirement must never be proposed for this page.

This is the hard veto on retirement — honor it regardless of evidence.

  • reader_signals / signals — Search Console and feedback-widget figures

from the optional reader-signals export; null when the export wasn't

available for the run (a signal-blind run says so, it never fabricates).

These fed the selection score and the composed "Why this page" block — they

are selection facts, not review instructions.

  • If articles is empty or halted is set, do nothing (the workflow won't

invoke you in that case, but be defensive).

  • traffic.available: false is not your problem to fix: the dispatcher's

degradation-health lane (scripts/content-review/signal-health.py) tracks

it — along with pulumi/pulumi-service (console-source) access, the

holiday feed, and the nightly claims re-verify — and alerts

docs-ops after a week of continuous degradation.

Process articles sequentially, one at a time, completing each article's

fix set and verdict before starting the next.

Per-article procedure

1. Branch — the workflow owns this

You do not create branches. The publish job derives the exact branch name

from the queue entry's slug and your verdict's retirement flag —

content-review/<slug> for a fix, content-review/retire-<slug> for a

retirement proposal (see below) — and a deterministic pre-check has already

skipped the run (with a skipped verdict) if an open PR owns either branch,

so you never need to check for one.

2. Pre-compute (deterministic floor) — the workflow runs this for you

The content-review-article workflow generates the synthetic whole-file diff

and runs the docs-review pre-steps before invoking you, exactly as the PR

review workflow does. The artifacts are already at the repo root when you

start: .fetched-urls.json, .candidate-claims.json, .verified-claims.json,

.vale-findings.json, .frontmatter-validation.json, and

.cross-sibling-discovery.json. Read them — do not regenerate them.

For reference (and for local runs outside CI), the exact pipeline the workflow

executes — scripts live in .claude/commands/docs-review/scripts/:

git diff --no-index /dev/null <path> > .synthetic.patch || true

python3 .claude/commands/docs-review/scripts/extract-urls-and-fetch.py \
    --patch-file .synthetic.patch --out .fetched-urls.json
python3 .claude/commands/docs-review/scripts/extract-claims.py \
    --patch-file .synthetic.patch --out .candidate-claims-regex.json
python3 .claude/commands/docs-review/scripts/extract-claims-llm.py \
    --patch-file .synthetic.patch --changed-files <path> \
    --pass atomic --scrutiny standard --out .candidate-claims-llm-1.json
python3 .claude/commands/docs-review/scripts/extract-claims-llm.py \
    --patch-file .synthetic.patch --changed-files <path> \
    --pass holistic --scrutiny standard --out .candidate-claims-llm-2.json
python3 .claude/commands/docs-review/scripts/merge-claims.py \
    --regex .candidate-claims-regex.json \
    --llm .candidate-claims-llm-1.json --llm .candidate-claims-llm-2.json \
    --out .candidate-claims.json
python3 .claude/commands/docs-review/scripts/verify-claims.py \
    --in .candidate-claims.json --fetched-urls .fetched-urls.json \
    --out .verified-claims.json
python3 .claude/commands/docs-review/scripts/frontmatter-validate.py \
    --changed-files <path> --out .frontmatter-validation.json
python3 .claude/commands/docs-review/scripts/cross-sibling-discover.py \
    --changed-files <path> --out .cross-sibling-discovery.json

Run Vale the way the review workflow does, in whole-file mode (no --pr):

vale --no-exit --output=JSON <path> > .vale-raw.json 2>/dev/null || echo '{}' > .vale-raw.json
python3 .claude/commands/docs-review/scripts/vale-findings-filter.py \
    --in .vale-raw.json --out .vale-findings.json || echo '[]' > .vale-findings.json

If any artifact is missing or carries an errors field, continue with the

artifacts you have and say so in the PR description; never fabricate artifact

contents. (Consult flag names with --help if running a script by hand —

do not guess alternate flags.)

3. Triage and fix — HIGH-CONFIDENCE ONLY

Read the artifacts and triage per

docs-review:references:pre-computation's contract (scripts find facts; you

make editorial judgments). The bar for applying a fix is strict. Apply

only:

  • Contradicted claims with an unambiguous correction.verified-claims.json

verdict contradicted or mismatch, where the authoritative source states

the correct value outright (a version number, a price, a flag name). If the

correction requires interpretation, do not apply it.

  • Dead or redirected internal links — fix to the canonical full path

(/docs/..., never ../).

  • Frontmatter violations from .frontmatter-validation.json (broken menu

parent, alias collision).

  • Deterministic-fix Vale findings — findings in .vale-findings.json stamped

deterministic_fix: true. The stamp (from the allowlist in

.claude/commands/docs-review/scripts/vale-deterministic-fixes.yaml — fixed

substitutions, canonical spelling, the closed-set cross-reference heading

rename) means Vale already knows the exact replacement, so you don't have to

author one. It does not mean apply blindly. For each, apply the

replacement named in the finding's message **only after confirming it

preserves meaning in this context** — read the surrounding sentence/section.

Skip and record under "Findings not applied" (with the reason) when the swap

would change meaning, e.g. clickselect inside "click event", a product

rename inside a historical quote, or a See also/Related block whose links

are actually sequential (that wants Next steps, not the rule's default

Learn more — apply the correct heading or flag it, don't mislabel intent).

This is verify-a-proposed-fix, not compose-a-fix; it is still a judgment.

Vale findings *without* the flag are style/judgment nags — leave them for the

human (see docs-review:references:spelling-grammar).

  • Readthrough local_repair findings.readthrough-findings.json

findings with fix_class: "local_repair" (per docs-review:references:readthrough).

Apply the finding's proposed_fix and nothing beyond it: reorder so a

prerequisite precedes its use, add a missing definition/step, split a

mixed-concept H2, delete a genuinely redundant passage, or surface a buried

outcome. The change stays inside the one page and preserves its purpose — that

bound is what makes it high-confidence. If applying it would mean touching

other files or reshaping the whole page, it isn't local_repair; treat it as

reconception.

Everything else — unverifiable verdicts, low-confidence corrections,

prose-quality findings, structural suggestions, **every readthrough finding with

fix_class: "reconception"** (a whole-page rewrite, cross-file split/merge, or

purpose change — flag, never auto-rewrite), anything you'd phrase with

"consider" — goes in the PR description's Findings not applied section

(one line of reasoning each), not in the diff. That list is the

almost-made-the-cut record the human reviewer adjudicates. When you flag a

reconception, set clarity_flag: true in the verdict sentinel (step 8) so the

ledger carries the signal even on an otherwise-clean page.

Record every fix you apply as an entry in the verdict sentinel's applied

array (step 8): its category, file, pre-fix line range, and a pointer to

the artifact finding it implements. The publish job deterministically verifies

that every hunk in your exported changes falls within the line range of a

recorded finding (scripts/content-review/verify-fix-scope.py); an edit

outside the recorded findings fails that gate and nothing is pushed — so if a

change doesn't trace to a finding above, don't make it.

Editing guardrails:

  • Never rewrite prose beyond the specific correction.
  • Stay inside the publish gate's scope: a fix review may touch only the

queued article itself plus the shared render-time sources named in step 6

(layouts/shortcodes/, layouts/partials/, data/); a retirement may

touch content/, scripts/redirects/, and data/docs_menu_sections.yml.

Any other changed path makes the gate reject the whole review.

  • A low_ctr_flag on the queue entry's signals.gsc block is FLAG-ONLY:

never rewrite title or meta_desc in response to it. The composer has

already pre-stubbed a "Search opportunity" row under Findings not applied —

keep it (you may add one line of observation, e.g. what the title fails to

say); a human runs /seo-analyze on it. Meta rewrites are the canonical

slop risk this restriction exists for.

  • Ordered lists keep their 1. numbering; files end with a newline; H1 Title

Case, H2+ sentence case (see STYLE-GUIDE.md — but don't re-case headings

that aren't otherwise wrong).

  • Never edit anything under a tier-0 (generated) path — selection excludes

them, but be defensive.

  • Never add the automation/merge label to anything.

4. Screenshot / UI pass (only when the page has images)

This pass is gated: the workflow pre-fills the PR's "Screenshot check"

section with "No images." when the source references no content images (a

deterministic source check — the shared meta_image card doesn't count). Run

this pass only when that section still carries a <TODO>. When you do, follow

references/screenshot-verification.md for every image the article references.

Verified-stale screenshots are flagged in the PR description (Screenshot

check section), never regenerated or deleted by you.

5. Validate

make lint must pass on your working tree. Fix what it surfaces; if you cannot, drop

the offending change rather than shipping a lint failure. **Do not run `make

build` here** — the full build is left to the PR's normal CI, and step 6 runs it

only on the pages that actually need the rendered pass.

6. Rendered content pass (only when the page assembles render-time content)

Source review misses content the page assembles at render time — shared

snippets from shortcodes, values from data/ files, partial-driven sections.

But most docs pages assemble nothing the source doesn't already show (plain

prose, code tabs, callouts, stepper chrome), so this pass is gated too: the

workflow pre-fills the "Rendered content" section with "Skipped" when the source

uses no content-bearing shortcode/partial/include, and leaves a <TODO> only

when one is present (it names the triggering shortcode). Run this pass only when

that <TODO> is there. When you do, first run make build (it produces the

rendered views), then check both:

HTML viewpublic/<url path>/index.html:

  • Extract the main content area's text (skip nav/footer/banner chrome).
  • Compare against the source markdown's prose. Rendered text **absent from

the source** is shortcode/data-sourced content — extract checkable

claims from exactly that residue (it's small) and verify them through

the same lanes as step 3.

  • Trace each residue finding to its origin: the shortcode call in the

page source → layouts/shortcodes/<name>.html / partials / data/

files. A fix at a shared source affects every page that includes it, so

shared-source corrections meeting the high-confidence bar may be

applied, and the PR description must flag them as multi-page

("also rendered on N other pages" — grep for other callers).

You do not check the markdown view (index.md) for leaked shortcode

delimiters here. Whether a shortcode renders cleanly to markdown is a property of

the shortcode, not the page, so it's covered once across the whole built corpus

by scripts/content-review/check-rendered-markdown.py (run periodically / in CI),

not re-paid per review. Content-*mangling* in the markdown output (a template

that silently drops or rewrites content) is a rendering-pipeline bug for the

templating owner, tracked separately — not something to fix in a content review.

If this pass applied any fix, re-run make build and then make lint (as

separate commands) before opening the PR.

7. PR body — only when you applied a fix

You do not open the PR — the publish job creates the PR to master whose body

is .pr-body-draft.md, exactly as you leave it. You do not write that body

from scratch: the workflow composed it (via compose-pr-body.py, the

assemble-then-judge model — the composer ASSEMBLES facts, you JUDGE) with every

section present and each pre-found finding pre-bucketed under a <TODO>. **Edit

that draft** in place, resolve every <TODO>, and strip the HTML-comment hints.

Before opening the PR the workflow runs the authoritative make lint on the

published branch and opens the PR ready for review only if lint passes

opening ready is what triggers triage and the docs review (a clean lint means it

flows through the normal pipeline; a trivial fix is short-circuited there). A

lint failure opens the PR as a draft instead, with a comment for a human; humans

merge. The sections (each is checked for):

  • Auto-merge notice (top > [!IMPORTANT] block): the re-lint gate arms

GitHub auto-merge on the PR, so the body flags that approving merges it.

Leave verbatim — do not move, reword, or remove it.

  • Why this page: composed from the selection queue (lane, tier, traffic

figure + period, Search/Reader-feedback figures when the reader-signals

export was available, last reviewed). Leave verbatim — do not

re-narrate it.

  • Fixes applied: pre-stubbed one row per high-confidence finding. Keep a row

only for a fix you actually applied (fill its Correction); move the rest down.

  • Findings not applied: pre-stubbed with the lower-confidence findings, plus

any row you moved down. One line of reasoning each. End the section with: "For

the judgment-level items above, run /glow-up <path>."

  • Screenshot check: per image — current / stale (what differs) /

unverifiable; note any aging reference screenshots (see

references/screenshot-verification.md).

  • Rendered content: outcomes of the rendered pass — residue claims

checked in the HTML view, the markdown view's shortcode-template status,

and any shared-source (shortcode/partial/data) findings with their

page-reach ("also rendered on N other pages").

  • Verification: the composer renders the pre-step artifact inventory; the

make lint result is stamped by the re-lint gate — leave the

<!-- LINT-RESULT --> line untouched. Note any pre-step that failed.

Do not record pr_number or head_sha yourself — the workflow derives

them from the branch it publishes. A clean article (zero applicable fixes)

gets no PR and needs no body edits — set "verdict": "clean" in the sentinel

(next step).

8. Verdict sentinel — your only structured output

Write .content-review-verdict.json at the repo root. This — plus your

working-tree edits and the PR body draft — is all you produce for the

workflow; do not write a ledger or a results file. The workflow derives the

PR facts (existence, number, head SHA) from the branch it publishes, builds

the canonical ledger record, and uploads it to S3 keyed by slug.

{
  "verdict": "fixed",
  "reason": "",
  "fixes": 2,
  "skipped_findings": 2,
  "retirement": false,
  "clarity_flag": true,
  "applied": [
    { "category": "claim", "file": "content/docs/iac/concepts/stacks/_index.md",
      "lines": [42, 43], "source": "verified-claims:c3" },
    { "category": "link", "file": "content/docs/iac/concepts/stacks/_index.md",
      "lines": [88, 88], "source": "dead link /docs/intro/concepts/state/" }
  ]
}
  • verdict: "fixed" (you applied fixes — the publish job opens the PR),

"clean" (zero applicable fixes, no PR), or "skipped" (a previous run

already owns this page's PR — normally stamped by the workflow's

deterministic pre-check before you even start).

  • reason: one line — required for clean and skipped; omit/empty for

fixed.

  • fixes: applied changes; skipped_findings: Findings-not-applied count.
  • retirement: true only for a retirement PR (branch

content-review/retire-<slug>).

  • applied: one entry per applied fix — category (one of claim, link,

frontmatter, vale, readthrough), file, lines ([start, end],

inclusive, pre-fix line numbers — the file as it was on master, the same

numbering the pre-step artifacts use), and source (the artifact finding it

implements, e.g. verified-claims:<claim_id>, vale:<rule>@L<line>,

readthrough:L40-58, or the dead link's old path). fixes should equal

len(applied). The workflow's scope gate cross-checks these against the

artifacts and the branch diff; for link fixes (which have no artifact) the

declared lines must actually carry the link in the pre-fix file.

  • clarity_flag: optional; true when you flagged a readthrough reconception

for this page. Carries onto the ledger record so the page's structural

follow-up is durable even when the verdict is clean or fixed (the

reconception itself lives in the PR's Findings-not-applied section). Omit when

there's no reconception to flag.

If you exit without writing this file, the workflow records the page as

incomplete. An incomplete outcome does not advance the staleness clock, so

the page stays due and is retried on a later sweep — up to an attempt cap, after

which it backs off for a human. Always write the sentinel, even for a clean or

skipped verdict.

Retirement proposals

For any article with "no_retire": false, retirement is a valid outcome

instead of a fix PR when the strict evidence standard below is met.

no_retire: true is the primary guardrail and an absolute veto — never propose

retiring such a page no matter how strong the evidence. The veto is

code-enforced twice in the publish job, before anything is pushed: the

publish gate rejects a retirement: true verdict for a page the queue stamps

no_retire, and scripts/content-review/check-retire-veto.py independently

re-derives the veto from strategic-tiers.yaml and the trusted dispatch

path input, so neither a model-edited queue nor a model-edited verdict can

clear it. Retirement is no longer

restricted to a particular lane; it can be proposed on any review that clears

the bar, with the full reasoning documented in the PR.

  • Evidence required (two-sided): the page appears in the traffic report

with near-zero views (absence from the report is NOT evidence — the page

may be new or alias-attributed), and GSC impressions/clicks are low

over its window when that data is present — read them from the queue

entry's signals.gsc block (a signals: null run has no GSC evidence, so

this leg cannot be satisfied); or the page is demonstrably

redundant with a named page (cite .cross-sibling-discovery.json). Check

the page's age in git — never propose retiring a page younger than a year.

  • Retire = redirect, never 404. The PR must redirect the page to its

superseding target: add the page's URL to the target's aliases: (or an

S3 redirect under scripts/redirects/ for non-Hugo paths), update inbound

internal links in /docs/, /product/, /tutorials/, and remove the

page + its menu entry. Follow move-doc reference mechanics.

  • Branch: set "retirement": true in the verdict sentinel — the publish

job derives content-review/retire-<slug> from it. The PR description

leads with the full evidence (traffic + GSC numbers and period, redundancy

target, inbound-link inventory).

  • When in doubt, don't propose retirement — review the page normally and

note the low-traffic observation under Findings not applied.

Output

The verdict sentinel (step 8), your working-tree edits, and the edited PR body

draft are your only outputs. The workflow publishes the branch, records the

ledger, and drives the docs review from them — there is no results file to

write.

Claims index and stale-claim boosts

Alongside the ledger record, the workflow persists the page's

.verified-claims.json to a claims index — one snapshot object per page

at claims/<slug>.json in the ledger bucket, written by

scripts/content-review/record-claims.py. Each kept claim carries the

entity_key / volatile fields stamped by the docs-review pipeline

(entity_key.py), so downstream consumers can join claims across pages by

the entity they assert something about.

The nightly claims-reverify.yml workflow re-checks volatile entities

(version pins, prices, limits) straight from this index

(scripts/content-review/reverify-claims.py). When an entity re-verifies

contradicted, every page asserting it gets a stale_claims marker in its

ledger entry, and select-articles.py boosts those pages to the front of the

next sweep — that is how a page can arrive in your queue the day after a

release changed a fact it states.

None of this is yours to write: this worker's whole-page runs are the index's

only writer, and the workflow runs record-claims.py itself after your

review. The markers clear automatically when your review's ledger and claims

rewrites land. Do not create, edit, or upload claims/ objects or

stale_claims fields.

How to use it

Copy the folder

Take pulumi/review-existing-content from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.