cc-os/plugins/os-doc-hygiene/PRD.md

18 KiB

PRD: doc-hygiene plugin

Status: shipped · Built at plugins/os-doc-hygiene/ in cc-os, not the cc-plugins target/marketplace named below. Historical design doc — kept as-is except this note and the location/marketplace corrections below.

Problem Statement

When I work intensively in a project, documentation accumulates two distinct defects that both degrade how usefully an AI agent (and I) can read the repo:

  • Stale docs — content that is now wrong: it contradicts the running system, references files/symbols/paths that no longer exist, is pinned to an old tool version, claims "in progress" for shipped work, or duplicates a fact that has since drifted. Trusting it causes wrong actions. This is the high-severity class because agents read confidently and don't sanity-check.
  • Bloated docs — content that is still true but mostly irrelevant: long debugging narratives, resolved-problem detail at full fidelity, decisions recorded without distillation, append-only growth. It doesn't lie; it dilutes attention and burns context-window budget, and buries the one live caveat in archaeology.

Severity scales with injection frequency: a stale line in a doc nobody opens is annoying; the same line in an auto-injected file (CLAUDE.md, memory index) misleads every session unprompted.

Today I notice this only incidentally, have no systematic way to find it across projects, and cleaning it up by hand wastes both my time and AI tokens. I also don't want a tool that interrupts my flow or silently rewrites my repos.

Solution

A Claude Code plugin, installed globally but operating per-project, that:

  1. Reminds, doesn't nag. A deterministic SessionStart hook checks an in-project timestamp and, only if the last hygiene check is older than a threshold, injects a noticeable reminder telling me how many days it's been and which command to run. Zero AI tokens unless I act. Snoozes so it reminds at most once per day while stale.
  2. Checks on demand. A check skill runs a deterministic scan that gathers objective signals and a candidate shortlist, then an AI classification pass that reads candidates, categorizes them (stale vs bloat, by sub-type), assigns each a concrete operation tagged by op-type (deterministic | generative) and safety tier (auto | confirm), and estimates context-weight savings. It produces a human report (what to clean and why) and a machine report (structured ops for the cleaner to consume).
  3. Cleans, git-safely and scoped. A clean skill consumes the machine report. For each op it checks an mtime guard (file unchanged since the check); deterministic ops are applied mechanically with no model; generative ops are delegated to Sonnet subagents. auto-tier ops run without fuss; confirm-tier ops escalate into an approval list. Cleanup runs only on a clean/committed tree (or after an auto-checkpoint) and lands as a single reviewable commit. The user can scope a run to specific categories or files.
  4. Composes. A sweep entry runs check-then-clean in one go, still surfacing the report and gating confirm-tier ops.

The design leads with deterministic code (scan, signals, state, patch-apply, token estimate) and reserves AI for the genuine judgment (classification and prose distillation). This keeps it fast, cheap, and trustworthy.

User Stories

  1. As a developer, I want a deterministic SessionStart reminder when a project's docs haven't been checked in over a threshold, so that I'm nudged without spending any AI tokens.
  2. As a developer, I want the reminder to be visually noticeable and to name the exact command to run, so that acting on it is one step.
  3. As a developer, I want the reminder to snooze (at most once/day while stale), so that it doesn't nag me every session for a week.
  4. As a developer, I want hygiene state stored per-project in a gitignored dot directory, so that there is no global index to corrupt, race, or itself go stale.
  5. As a developer, I want to run a check manually with a slash command, so that after a heavy context-building session I can clear what accumulated.
  6. As a developer, I want the check to deterministically scan scoped files and compute objective signals (broken references, version skew, edit-recency vs git churn, location, append-only growth, archive-to-live ratio, frontmatter markers), so that the AI pass starts from facts, not a blank read.
  7. As a developer, I want the AI pass to classify each candidate as stale (contradicted, orphaned, superseded, provisional, completed-in-place, duplicated) or bloat (distill, split, freeze), so that the right remedy is chosen per file.
  8. As a developer, I want each recommended operation tagged with an op-type (deterministic | generative) and a safety tier (auto | confirm), so that the cleaner knows what it can apply mechanically and what needs a model or my approval.
  9. As a developer, I want a human-readable report describing what should be cleaned and why, grouped by category, so that I can decide at a glance.
  10. As a developer, I want a machine-readable report the cleaner consumes directly, so that the cleanup step doesn't re-derive the analysis.
  11. As a developer, I want only the single most-recent report kept (rollover deletes prior reports), so that the tool's own artifacts don't become bloat.
  12. As a developer, I want the check to estimate context-weight savings per file, summed into categories and a total (bottom-up), so that I can prioritize.
  13. As a developer, I want savings honestly framed and weighted by injection frequency (auto-injected files counted as real per-session savings, on-demand docs as theoretical-max), so that the numbers aren't misleading.
  14. As a developer, I want the cleaner to apply deterministic+auto ops (move-to-archive, freeze-stamp, known-target link fix, exact-dup dedupe) with no model and no prompt, so that no-brainer cleanup just happens.
  15. As a developer, I want every destructive, subjective, or generative op to be confirm-tier and escalated for my approval, so that nothing surprising happens to my repos.
  16. As a developer, I want a per-op mtime guard so that if a file changed since the check, its cached edit is skipped (and re-analysis recommended) rather than blindly applied.
  17. As a developer, I want generative cleanup (distilling narrative into live constraints) delegated to a Sonnet subagent, so that distillation quality is high and doesn't drop the constraint that mattered.
  18. As a developer, I want cleanup to run only on a clean/committed tree, or to auto-commit a WIP checkpoint first, so that my uncommitted work is never lost.
  19. As a developer, I want each cleanup run to land as a single reviewable commit, so that I can inspect or revert the whole sweep trivially.
  20. As a developer, I want to scope a cleanup to specific categories or files (e.g. "only the orphaned-reference fixes"), so that I stay in control of how much changes at once.
  21. As a developer, I want a sweep command that runs check-then-clean, so that when I already trust the project I can do both in one step.
  22. As a developer, I want sweep to still surface the report and gate confirm-tier ops, so that the convenience path isn't a foot-gun.
  23. As a developer, I want a status command that reports last-check and last-clean timestamps for the current project, so that I can see where it stands without running anything.
  24. As a developer, I want to protect files from being flagged via hygiene: frozen frontmatter or a .dochygiene-ignore file, so that deliberately-frozen records and append-only logs aren't repeatedly flagged.
  25. As a developer, I want sensible scope defaults (markdown + known doc locations; excluding build/vendor/archive/graphify-out dirs) with per-project overrides, so that the scan targets docs and nothing irrelevant.
  26. As a developer, I want the check and clean steps to update their own timestamps (last_check, last_clean) and the reminder to track last_reminded, so that the lifecycle is observable and the snooze works.
  27. As a developer, I want the plugin to work in any project where it's enabled without per-project setup beyond enablement, so that adoption is frictionless.
  28. As a developer, I want classifier behavior protected by golden examples and invariants, so that future edits to the plugin don't silently regress what counts as stale vs bloat.
  29. As a developer, I want decisions (especially confirm-tier approvals) recorded, so that there's an audit trail of what was changed and why.
  30. As a developer, I want the deterministic scripts to emit structured JSON and correct exit codes, so that they're independently testable and composable.

Implementation Decisions

Distribution & activation

  • New plugin os-doc-hygiene in the cc-os marketplace (built at plugins/os-doc-hygiene/; see migration note above). Installed globally, operates per-project. No global state index — the question of "how to key projects globally" is dissolved by per-project storage.
  • Marketplace registration deferred until the plugin is functional (documented as a build-completion step, not done now).

State & artifacts (in-project)

  • A gitignored .dochygiene/ directory at the project root (resolved via git root, fallback cwd) holds:
    • state.jsonlast_check, last_clean, last_reminded timestamps.
    • the single most-recent report (human .md + machine .json).
  • Rollover: each new check deletes the prior report before writing the new one.
  • .dochygiene/ must not be tracked. The tool does not silently edit the user's .gitignore (that would itself be an outward mutation, against the non-intrusive premise). Instead: on first check it detects whether the dir is ignored and, if not, surfaces a one-line offer to add the entry — applied only on confirmation (or noted in the report). The scanner always self-excludes .dochygiene/ regardless.

Components / modules (described abstractly; see CLAUDE.md for the build map)

  • State store (deterministic): reads/writes state.json; atomic writes; resolves project root.
  • Scanner (deterministic): walks scoped files, computes signals, emits a candidate shortlist with attached signals. Respects scope config + ignore markers + frozen frontmatter + append-only detection.
  • Reminder hook (deterministic, SessionStart): reads state, compares to staleness threshold, applies once/day snooze via last_reminded, emits a noticeable notice. No model.
    • Mechanism (confirmed): plugins declare hooks in hooks/hooks.json at the plugin root (not under .claude-plugin/). The visible, zero-token notice is emitted via the hook's JSON systemMessage field (a user-facing banner) — NOT additionalContext (which is silent to the user). The command names the slash command to run. matcher is startup|resume.
    • Snooze is load-bearing, not polish: SessionStart also fires on resume, clear, and compact. The once/day last_reminded snooze is what prevents the banner re-firing after every compaction within a single working session.
    • Constraints to respect: keep timeout low (≤5s); exit 0 (never block the session); the script must be fast and side-effect-free beyond reading state.
  • Classifier / report builder (AI — the check skill): consumes the shortlist, reads candidates, assigns category + op-type + safety tier, writes exact edits for deterministic ops, and calls the token estimator. Emits human
    • machine reports. Updates last_check.
  • Token estimator (deterministic helper): counts tokens of removed/reduced spans with a local tokenizer approximation (no API call); weights by injection frequency; rolls up file → category → total. Framed as "context weight reduced," splitting injected vs on-demand.
  • Cleanup executor (the clean skill + deterministic patch-applier): consumes the machine report; mtime-guards each op; applies deterministic ops mechanically; delegates generative ops to Sonnet subagents; runs auto freely and escalates confirm; enforces git checkpoint + single-commit; scopable by category/file. Updates last_clean.
  • Sweep (composition): invokes check then clean, preserving the report surface and confirm gate.

Operation taxonomy

  • Op-type deterministic: exact, reversible edits the check pre-computes (delete/move/stamp/known-link-fix/exact-dedupe). Applied with no model.
  • Op-type generative: prose transformations (distill, split) requiring a model at clean time (Sonnet). Deferred — not pre-written at check time — so a check is cheap even when no clean follows.
  • Safety tier auto: deterministic + reversible + objective → runs without prompt.
  • Safety tier confirm: destructive (any delete), subjective, or generative → escalated for approval.

Git safety (reversion-protection vocabulary)

  • Cleanup requires a clean/committed working tree, or auto-commits a WIP checkpoint first.
  • Each cleanup run lands as one reviewable commit (the post-state).
  • confirm-tier approvals are logged to a decisions record.

Model routing (per the marketplace convention)

  • Deterministic scan/state/patch-apply/token-estimate: no model (scripts).
  • Writing those scripts: Haiku.
  • Classification / report building: Sonnet (reading comprehension + judgment). The hardest stale-vs-bloat distinctions may escalate to Opus.
  • Generative distillation at clean time: Sonnet (explicitly not Haiku — this is the highest-judgment task and Haiku produces lossy summaries).
  • Orchestration: Opus (the calling Claude).

Language

  • Python, OOP, small composable single-responsibility classes with dependency injection (per the deterministic-scripting reference), chosen for broad reach in a general-purpose plugin and for testability.

Report schema is the linchpin

  • The machine report schema (per-file: category, signals, recommended op, op-type, safety tier, optional exact-edit, token estimate) is the contract every other component consumes. It is designed and frozen first.

Testing Decisions

What makes a good test here: assert external behavior at the highest deterministic seam — given an input doc tree / state file / report, the script produces the correct structured output and exit code. Do not assert internal class structure. The AI classification layer is pinned by golden examples, not unit assertions.

Seams (highest first):

  • Scanner — input: a fixture doc tree; output: candidate shortlist JSON. Unit-tested against fixtures exercising each signal (broken ref, version skew, churn-vs-edit, location, append-only, frozen frontmatter, ignore file).
  • State store — timestamp read/write, rollover (only latest report kept), snooze logic. Unit-tested with an injected clock and temp dirs.
  • Reminder hook — given a state.json + injected clock + threshold, asserts notice / no-notice / snoozed. Unit-tested via the injected clock (no real time, no real session).
  • Token estimator — counts on known spans; injection-frequency weighting; bottom-up rollup. Unit-tested on fixed inputs.
  • Patch-applier + mtime guard — applies deterministic ops on fixtures; skips when fixture mtime is newer than the check timestamp. Unit-tested.
  • Classifier (AI) — golden examples (examples/golden/): input doc tree → expected report categorization, per the reversion-protection pattern. Run the check against each; flag mismatches for human review.

Prior art: the commit and cc-architect plugins pair deterministic scripts/ with skills and structured output; this plugin follows that split. The reversion-protection reference (cc-architect/.../reversion-protection.md) defines the golden-example + invariants approach used for the classifier.

Out of Scope

  • A global cross-project dashboard / index (dissolved by per-project storage).
  • Exact Claude token counting via API at check time (local approximation only; exactness isn't worth the latency/cost for a motivational estimate).
  • Non-markdown content analysis beyond known doc locations (e.g. linting source code comments). Scope defaults to docs.
  • Auto-running cleanup from the SessionEnd/SessionStart hook — the hook only ever reminds; all mutation is user-invoked.
  • Pushing commits or any outbound/network action — cleanup is local commits only; the user pushes.
  • Editing files outside the resolved project root.
  • v2 polish: richer per-category savings visualizations, configurable op taxonomies beyond the built-in set.

Further Notes

  • Build order (the report schema gates everything): (1) machine report schema; (2) deterministic scanner + state store + reminder hook; (3) check skill (scanner → AI classify → report); (4) clean skill (consume report, git-safe, scoped) + sweep; (5) bonus: deterministic patch-apply + token estimator. Patch-apply and token-estimate are explicitly v2 — they optimize a report format that must exist and be proven first.
  • Why deterministic-first matters here specifically: the check does the expensive cognition once (reading + deciding); cleanup executes. Pushing attribute-detection into the scanner and exact edits into the report means most cleanup needs no model at all, and a check is cheap even when no cleanup follows.
  • False-positive trust risk: without the frozen/ignore mechanism and append-only detection, the tool would re-flag deliberately-frozen records every week and lose the user's trust. This is treated as a correctness requirement, not a nicety.
  • The tool must not become bloat: report rollover (keep latest only) and the gitignored dot dir are deliberate guards against the plugin polluting the very repos it cleans.
  • Linchpin mechanism verified: the SessionStart systemMessage banner path was confirmed against the Claude Code hook docs before this PRD was finalized (no other plugin in the collection wires a CC hook, so it had no precedent). Build-spike #1 is still to stand up a trivial hooks/hooks.json emitting a systemMessage and confirm it renders visibly in a real session before building the deterministic core on top of it.