Prevent doc drift
A per-PR doc-quality skill. It compares the PR diff against agent guidance files and updates them in the same PR so future agents are never working from a stale map.
This skill is paired with .cursor/skills/maintain-docs/:
prevent-doc-drift(this skill) — runs on every PR; catches drift at the moment it's introduced.maintain-docs— runs periodically across the whole repo; catches drift that this skill missed.
Together they form a two-tier defence: the per-PR skill is the primary line, the periodic skill is the safety net.
Hard constraints
These constraints are absolute and override any other instructions:
- Only modify documentation files. Allowed:
AGENTS.md,CLAUDE.md,README.mdat repo root; any*.mdor*.mdcunder.cursor/rules/,.cursor/skills/,.claude/skills/, ordocs/developer/. Forbidden: any source file (.ts,.tsx,.js,.jsx,.go,.json,.css,.html), config files, lockfiles, and any file underdocs/design/(design docs are author-curated by humans via thedesign-reviewskill). - Operate on the PR diff only. Do not audit files outside the diff. If you find drift in a file the PR did not touch, add it to
docs/_maintenance-backlog.mdformaintain-docsto handle — do not silently fix it here. - Never fabricate. Only document features, subsystems, services, scripts, and relationships that the diff actually adds. If you cannot confirm something exists from the diff plus a focused code read, omit it.
- Before staging anything, verify the constraint. Run
git diff --name-onlyafter edits and confirm every changed path is in the allowed list above. Abort if any disallowed file appears. - One commit max. All doc updates land as a single follow-up commit on the same PR branch (or, if invoked from
/review, as a recommended diff block in the review output — see "Operating modes" below). - Do not amend the PR's existing commits. Create a new commit.
Operating modes
The skill has two modes:
- Apply mode (default when invoked directly): the working tree is the PR branch. Apply edits, stage, commit with a clear message, push.
- Review mode (invoked from
/review): emit one canonical observation with the proposed patch evidence. Do not modify the working tree. The shared review policy decides its disposition.
Decide which mode applies based on context: if a gh pr command can determine the current branch is a PR branch and the user invoked this skill directly, use apply mode. If the parent is /review, use review mode.
Workflow
Phase 0 — Resolve the diff
- Determine the base ref:
- If on a PR branch:
gh pr view --json baseRefName -q .baseRefName - Otherwise:
git merge-base HEAD main(or whatever the repo's main branch is — checkgit symbolic-ref refs/remotes/origin/HEAD).
- If on a PR branch:
- Capture the diff in three forms — you'll use each:
git diff --name-status <base>...HEAD(status flags A / M / D / R)git diff --stat <base>...HEAD(line counts)git log <base>..HEAD --oneline(commit subjects)
- From
package.json, read the current set of npm scripts:jq -r '.scripts | keys[]' package.json(you'll diff this against the table in AGENTS.md).
If the diff is empty or doc-only, exit cleanly with "no source changes — no drift possible."
Phase 1 — Categorize changes
Walk the name-status output and bucket each path into one of these categories. A single file may land in multiple buckets; that's fine.
| Bucket | Matches (path patterns) |
|---|---|
| New frontend subsystem | New directory at src/<dir>/ (any first commit of a path matching src/<name>/...) |
| New backend file | New file at pkg/**/*.go |
| New developer doc | New file at docs/developer/**/*.md |
| New design doc | New file at docs/design/**/*.md (we index it; we do not edit it) |
| New rule | New file at .cursor/rules/*.mdc or .cursor/rules/*.md |
| New skill | New directory at .cursor/skills/<name>/ (look for new SKILL.md) |
| New npm script | Diff of package.json adds a key under .scripts |
| New plugin HTTP route | Diff of pkg/plugin/resources.go adds a mux.Handle(...) or mux.HandleFunc(...) call |
| New plugin HTTP route consumer | Diff of src/integrations/coda/coda-api.ts adds a call to a grafana-coda-app v1 route |
| New feature flag | Diff of src/utils/openfeature.ts or related adds a flag name |
New data-test-* attribute |
Diff adds a new test-id constant in src/constants/testIds.ts or sets a new data-test-* literal |
| Renamed / moved subsystem | Name-status R<percent> entries that change a src/<dir>/ or pkg/plugin/<file> path |
| Removed subsystem | Name-status D entries that empty out a src/<dir>/ or pkg/plugin/<file> path |
| Tier rule edit | Diff of src/validation/architecture.test.ts or relevant ESLint config changes |
For each match, capture the path(s), surrounding diff context, and one-sentence summary of what the change does (read up to ~30 lines of the new file or the diff hunk if needed).
Phase 2 — Detect required doc updates
For each bucket with hits, consult the rules table below. Each rule lists the target doc file(s) and exactly what edit is required.
| Bucket | Target file(s) | Required edit |
|---|---|---|
| New frontend subsystem | AGENTS.md |
Add the directory to the inline tier list in "Frontend tier model" under the correct tier (Tier 0 types, Tier 1 support, Tier 2 engines/hooks, Tier 3 integrations, Tier 4 UI). AGENTS.md carries only the one-line tier summary; the annotated tier definitions and the "Key dependency edges" table live in .cursor/rules/systemPatterns.mdc — if the subsystem participates in a load-bearing import edge, add the row there (see the next row). |
| New frontend subsystem | .cursor/rules/systemPatterns.mdc |
Add a one-paragraph entry in "Frontend subsystem reference" (purpose, entry point, tier), placed under the correct tier sub-heading. |
| New frontend subsystem | docs/developer/CONTEXT_INDEX.md |
Add a row in the most relevant section (e.g., "Subsystem references") with path pattern (src/<name>/*), when-to-load description, and glob trigger (for the "Auto-triggered by globs" column — Cursor metadata, see CONTEXT_INDEX preamble) if the subsystem has a natural file-glob trigger. |
| New backend file | .cursor/rules/systemPatterns.mdc |
Add a row to the pkg/ "File map" table in "Backend architecture (pkg/)" with a one-line role description. AGENTS.md's "Backend (pkg/)" section is intentionally a short prose summary and is not the per-file catalog. |
| New developer doc | AGENTS.md |
If broadly load-bearing, add a bullet to the hot-paths list in "On-demand context". Otherwise just rely on the next row (CONTEXT_INDEX is the canonical routing table). |
| New developer doc | docs/developer/CONTEXT_INDEX.md |
Verify a row exists in the relevant section pointing at the new doc; add one if missing, with a when-to-load description and glob trigger if applicable. |
New developer doc with .cursor/rules/ counterpart |
.cursor/rules/<counterpart>.mdc |
Add a "For full reference, see docs/developer/<file>.md" link near the top of the rule file. |
| New design doc | docs/developer/CONTEXT_INDEX.md |
Add a row in "AI-authoring design docs" (or the closest matching section) with the note "Design intent (may not match implementation)" in the when-to-load column. |
| New rule | docs/developer/CONTEXT_INDEX.md |
Verify a row exists in "Architecture and project context" (or the relevant section) with glob trigger from the rule's frontmatter; add one if missing. If the rule is broadly load-bearing, also add a bullet to the hot-paths list in AGENTS.md's "On-demand context". |
| New skill | .claude/skills/<name>/SKILL.md |
Verify the new skill has both halves: the body under .cursor/skills/<name>/SKILL.md and a pointer stub at .claude/skills/<name>/SKILL.md whose name + description frontmatter matches the body verbatim. src/validation/skill-references.test.ts enforces this. No AGENTS.md, CLAUDE.md, or CONTEXT_INDEX skill enumeration to maintain — frontmatter is the single source of truth. |
| New npm script | AGENTS.md |
If the script belongs in the short list at the top of "Essential commands", add it there. Otherwise rely on docs/developer/COMMANDS.md (the canonical full command reference, which AGENTS.md links to). |
| New plugin HTTP route | .cursor/rules/systemPatterns.mdc |
Document the route under "Backend architecture (pkg/)" — extend the plugin/resources.go row in the file map or the request-flow narrative below it with method + path + one-line purpose. AGENTS.md's pkg/ summary stays prose. |
| New plugin stream message type | .cursor/rules/systemPatterns.mdc |
Add to the "Stream message types" bullet under "Backend architecture (pkg/)". |
| New feature flag | docs/developer/FEATURE_FLAGS.md |
Add the flag — name, type, default, what it controls. |
New data-test-* attribute |
docs/developer/E2E_TESTING_CONTRACT.md |
Add the new selector to the relevant section. |
| Renamed / moved subsystem | All doc files | grep -lr <old-path> AGENTS.md CLAUDE.md .cursor/rules/ docs/developer/ and rewrite each occurrence to the new path. |
| Removed subsystem | AGENTS.md, .cursor/rules/systemPatterns.mdc, others |
Remove the corresponding rows / paragraphs. If the removed feature represents an epic-scale change, recommend (in the PR description) a follow-up note in docs/history/. |
| Tier rule edit | AGENTS.md, .cursor/rules/systemPatterns.mdc |
If the architecture allowlist changed, update the one-line tier summary in AGENTS.md and the annotated tier model in .cursor/rules/systemPatterns.mdc to reflect the new exception with one sentence of justification. |
CONCERNS.md coverage check (read-only): docs/design/CONCERNS.md is author-curated and forbidden from edits. However, for every new frontend subsystem detected, run:
grep "src/<new-dir>/" docs/design/CONCERNS.md
If no concern's trigger_paths covers the new directory, the subsystem has no review routing. Surface this gap in one of two ways, depending on mode:
- Apply mode: append a backlog item to
docs/_maintenance-backlog.md(Phase 5):YYYY-MM-DD: No CONCERNS.md concern covers src/<new-dir>/ — consider adding one or extending the nearest subsystem concern. - Review mode: include this checked gap in the observation evidence and make the suggested action:
Add src/<new-dir>/ to the nearest concern, <closest-matching-concern-id>.
To identify the closest matching concern, scan the trigger_paths column for the concern whose paths share the most path segments with src/<new-dir>/.
Sources of truth for the rules table: when in doubt, prefer the code over any doc. If a rule says "add a row to the src/ tree" but the tree is already accurate (because the contributor pre-emptively updated it), do nothing — log "no edit needed" and move on.
Phase 3 — Generate the edits
For each required edit:
- Read the target file's current content (or the relevant section).
- Compose the minimal change needed — a new row, a new bullet, a new paragraph. Match the surrounding format exactly: column alignment in markdown tables, sentence case per the project's writing style (see AGENTS.md), backtick conventions for code identifiers.
- Sentence case: capitalize only the first word and proper nouns (Grafana, Loki, Prometheus, Tempo, Mimir, Alloy, Grafana Cloud, etc.). Never title case for headings, button labels, or table cells.
- No emojis unless the existing file already uses them.
- Avoid touching unrelated lines. Surgical edits only.
If multiple rules target the same file (e.g., several new scripts), batch their edits into one Edit operation when possible.
Phase 4 — Apply or report
Apply mode:
Make the edits via
Edit/Write.Run
git diff --name-onlyand confirm every changed path is in the allowed list (per hard constraint #4). Abort and revert if not.Run
npm run prettierto format markdown.Run
git diff --name-onlyagain after prettier; verify only doc files changed.Stage the changed doc files (do not use
git add -A; stage by explicit path).Commit with a clear message:
docs: keep agent guidance in sync with this PR's changes - <one bullet per change> Generated by .cursor/skills/prevent-doc-drift.Do not push or open a separate PR — this commit rides the existing PR branch.
Report a summary back to the user: which buckets were detected, which doc files were updated, line counts.
Review mode (invoked from /review):
Do not modify the working tree. Emit one observation using Canonical observation from docs/design/PR_REVIEW.md:
finding_id: a stabledoc-drift-<bucket>identifierconcern_id:documentation-alignmentkind:defectseverity:lowconfidence:highonly when the changed contract and stale guidance are both checkedevidence: changed contract evidence plus one proposed fenced patch per affected filewhy_it_matters: the specific stale instruction a future contributor or agent would followsuggested_action: apply the minimal proposed documentation editsreversibility:reversibleapplies_to_files: affected guidance filesorigin:regressionimpact:nonetiming: supplied by the parent reviewscope_effect:widens_changed_surfaceunless every affected guidance file is already changedbreaks_shipped_path:falseinduced: whether the drift exists only because optional earlier review advice was implemented
Do not supply or imply a disposition. The parent runs the observation through review-policy.mjs.
Phase 5 — Backlog handoff
If you detect drift the skill cannot fix (because it's outside the diff, or the change is too large for incremental editing), append to docs/_maintenance-backlog.md under "Work items":
- YYYY-MM-DD: <one-line description>. Rationale: <why prevent-doc-drift deferred>.
This hands the issue off to maintain-docs on its next run.
Detection heuristics (deeper guidance)
Detecting a "new frontend subsystem"
A directory under src/<name>/ is a subsystem if any of the following hold:
- It has its own
index.tsbarrel export - It is referenced in
AGENTS.md's "Frontend tier model" bullet list (existing subsystems) - It contains a
README.md - It is imported from at least two other
src/<other>/directories
If the diff only adds files inside an existing subsystem, this is not a "new subsystem" — it's an internal addition. Decide based on the directory structure, not the file count.
Detecting which tier a new subsystem belongs in
The canonical tier model lives in TIER_MAP in src/validation/import-graph.ts and is documented in AGENTS.md "Frontend tier model" and .cursor/rules/systemPatterns.mdc "Frontend tier model". Imports flow downward only (higher tier may import lower; never the reverse).
Tier 2 markers (engines & hooks):
- The name contains
-engine,-manager,recovery, or a similarly load-bearing identifier (note:recoveryitself currently lives in Tier 1 because it depends only ontypes/— exact placement is dictated by what the subsystem actually imports, not by name alone) - It exports a service class or a hook that other subsystems depend on
- It is imported by
components/(Tier 4) orpages/(Tier 4)
Tier 1 markers (support utilities):
- Imports only
types/andconstants/(Tier 0) - Provides utility functions, not orchestration
- Not imported by
components/directly (or imported only as a low-level helper)
When uncertain, default to Tier 1 — over-promotion (placing a new subsystem too high in the tier list) creates spurious "may import from anything" expectations that are harder to roll back than under-promotion. The architecture test (src/validation/architecture.test.ts) will catch any tier-doc mismatch on next CI run.
Detecting renames in the diff
git diff --name-status shows renames as R<percent> followed by old-path and new-path, e.g. R98 src/old-engine/foo.ts src/new-engine/foo.ts. Treat any rename with similarity ≥ 75 as a true rename worth propagating to docs.
Sentence-case checking
Before committing, scan your edits for title-case violations:
- Headings: only the first word capitalized (plus proper nouns)
- Table cells: same rule
- Description text: same rule
The repo follows the Grafana Writers' Toolkit. Proper nouns to capitalize: Grafana, Loki, Prometheus, Tempo, Mimir, Alloy, Grafana Cloud, Grafana Enterprise, Grafana Labs. Generic terms stay lowercase: dashboard, alert, data source, panel, query, plugin.
When to exit cleanly without making changes
- The diff is doc-only (no source changes).
- The diff contains only test-only changes, lint cleanups, dependency bumps, or version bumps — none introduce features.
- The contributor pre-emptively updated the docs and the audit produces no required edits.
In each case, report briefly: "No drift detected — exiting cleanly."
When to NOT run this skill
- Branches that are not PR branches (running on
maindirectly). - PRs that explicitly bypass the doc-drift contract (e.g., emergency hotfixes labelled
hotfix-no-docsor similar). If the PR has such a label, exit cleanly with a note. - When the user has explicitly asked for source-code-only changes — in which case any doc edits are out of scope.
Integration with /review
When /review runs, fold this skill into a routed worker or the root and use review mode. It emits a canonical no-impact defect and stays silent when no drift exists. review-policy.mjs owns disposition and review-report.mjs owns publication. The PR author can apply the suggested edits or invoke this skill in apply mode.
Examples
Example 1: PR adds a new engine
PR diff includes:
A src/recommendation-cache/index.ts
A src/recommendation-cache/cache.ts
A src/recommendation-cache/cache.test.ts
M src/context-engine/context.service.ts (imports new cache)
Detected buckets: New frontend subsystem.
Detected updates:
AGENTS.md"Frontend tier model" — addrecommendation-cache/to the Tier 2 bullet (engines & hooks) and add the edgecontext-engine→recommendation-cacheto the "Key dependency edges" table in the same section.cursor/rules/systemPatterns.mdc"Frontend subsystem reference" — add a paragraph entry under the Tier 2 sub-heading (purpose, entry point, tier)
Example 2: PR adds a new npm script and feature flag
PR diff includes:
M package.json (+ "validate:e2e": "...")
M src/utils/openfeature.ts (+ "useNewRecommender" flag)
Detected buckets: New npm script, New feature flag.
Detected updates:
AGENTS.md"Essential commands" — addnpm run validate:e2eonly if it qualifies as a top-of-file essential command; otherwise log "no edit needed — script is documented indocs/developer/COMMANDS.md(canonical full command reference)"docs/developer/FEATURE_FLAGS.md— adduseNewRecommenderto the flag table
Example 3: PR adds a new plugin HTTP route
PR diff includes:
M pkg/plugin/resources.go (+ mux.HandleFunc("/sessions", a.handleSessions))
M pkg/plugin/sessions.go (new file)
Detected buckets: New backend file, New plugin HTTP route.
Detected updates:
.cursor/rules/systemPatterns.mdc"Backend architecture (pkg/)" — addplugin/sessions.gorow to the file map with a one-line role description, and add the newPOST /sessionsroute to the request-flow narrative (or extend theplugin/resources.gorow note)AGENTS.md"Backend (pkg/)" — no edit needed unless the route changes the three primary request paths (HTTP resource API, streaming terminal, Coda JWT client) summarized in the section's prose
Example 4: contributor already updated docs
PR diff includes a new subsystem and the corresponding rows in AGENTS.md and .cursor/rules/systemPatterns.mdc.
The skill detects the new subsystem (Phase 1), runs the rules (Phase 2), then in Phase 3 reads each target file and finds the rows already present. It logs "no edit needed" for each and exits cleanly in Phase 4.
Context window management
This skill stays small intentionally:
- Phase 0 reads only the diff (one
gh/gitinvocation each). - Phase 1 categorizes from the diff without reading full file contents (~30 lines max per new file).
- Phase 2 reads only the target doc sections that need editing — never the whole file unless < 100 lines.
- Phase 3 makes surgical edits via
Edit(notWrite). - Phase 4 runs prettier and verifies the changed-file list, then commits.
Total context budget per run: under 30k tokens for a typical PR. Larger PRs may exceed this — in that case, prioritize: (1) new subsystems, (2) new docs, (3) new scripts, (4) everything else.