2026-07-03 15:12:31 +00:00
|
|
|
# PRD: `doc-hygiene` plugin
|
|
|
|
|
|
2026-07-16 19:34:41 +00:00
|
|
|
> 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.
|
2026-07-03 15:12:31 +00:00
|
|
|
|
|
|
|
|
## 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**
|
2026-07-16 19:34:41 +00:00
|
|
|
- New plugin `os-doc-hygiene` in the cc-os marketplace (built at
|
|
|
|
|
`plugins/os-doc-hygiene/`; see migration note above). Installed globally,
|
2026-07-03 15:12:31 +00:00
|
|
|
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.json` — `last_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.
|