Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/docs-hygiene/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "docs-hygiene",
"version": "0.14.7",
"version": "0.15.0",
"description": "Documentation-hygiene toolkit: compress (flavor-trim markdown with a semantic-diff safety net), audit-noise (classify markdown noise), extract-ssot (deduplicate repeated content into a single source of truth), audit-encapsulation (detect citations into skill-private surfaces), rename-references (sweep stale references after renames), and audit-derivability (classify whether a whole document earns its existence \u2014 could a fresh agent re-derive it from the code?).",
"author": {
"name": "Melodic Software",
Expand Down
17 changes: 17 additions & 0 deletions plugins/docs-hygiene/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog — docs-hygiene plugin

## [0.15.0]

### Fixed

- **compress (plugin-quality audit #2745):** rewrite caveman Step B as
cross-tool-call steps (no EXIT trap / non-persistent `$tempdir`); map
detector `unknown` → Edit fallback; require `enabled: true` (prefer
`caveman@caveman`) in `detect-caveman.sh`; fix pre-computed `|| echo none`
pipeline; name audit-table destination under `${CLAUDE_PLUGIN_DATA}/audit/`;
reword signal 6 as an owned curated token list; annotate taxonomy/LATITUDE
drift (batch = word-level; Edit fallback = full matrix); note drifted-skill
matrix niche is unreachable via signal 1; add yield circuit breaker + top-10
interview default; ship `scripts/audit-scan.sh` + contract tests; point eval 8
at `evals/fixtures/terse-agent.md`; widen fixture-gate conventions; soft-block
wording in `integration.md`; record deliberate `disable-model-invocation:
false`.

## [0.14.7]

### Added
Expand Down
46 changes: 25 additions & 21 deletions plugins/docs-hygiene/skills/compress/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,46 @@
## Pre-computed context

Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"`
Uncommitted .md files: !`git status --porcelain 2>/dev/null | grep '\.md$' | head -10 || echo "none"`
Uncommitted .md files: !`{ git status --porcelain 2>/dev/null | grep '\.md$' || echo "none"; } | head -10`

## Purpose

Markdown in `docs/`, README files, onboarding docs, third-party pasted prose, and drifted skill bodies accumulates FLAVOR — filler ("just", "really", "basically"), hedging ("perhaps", "might"), articles, pleasantries, redundant restatement. `context/flavor-vs-content-matrix.md` defines FLAVOR (safe to cut) vs CONTENT (never cut); this skill applies that taxonomy AT EDIT TIME to content where author-time discipline does NOT apply.

Always-loaded instruction files (`.claude/rules/**`, `AGENTS.md`, `CLAUDE.md`, `**/SKILL.md`) bound empirically at 2-3% yield (see ## Sources). Likely 5-15% yield on author-time-undisciplined content.
Markdown in `docs/`, README files, onboarding docs, third-party pasted prose, and drifted skill bodies accumulates FLAVOR — filler ("just", "really", "basically"), hedging ("perhaps", "might"), articles, pleasantries. `context/flavor-vs-content-matrix.md` defines FLAVOR (safe to cut) vs CONTENT (never cut). The **batch fan-out path** (Phase A LATITUDE) is a word-level trimmer: mechanical drops + passive→active + nominalization only — no sentence-level restatement deletion. The **single-file in-session Edit fallback** may apply the full matrix taxonomy (including redundant restatement of bold rule names) behind the same semantic-diff net. Always-loaded instruction files (`.claude/rules/**`, `AGENTS.md`, `CLAUDE.md`, `**/SKILL.md`) bound empirically at 2-3% yield (see ## Sources). Likely 5-15% yield on author-time-undisciplined content when the Edit fallback's broader latitude applies; batch fan-out yields are correspondingly smaller.

Methodology: snapshot original → backend mechanical compression (the `caveman` plugin via `/caveman:compress`, OR in-session Edit fallback) → spawn semantic-diff subagent comparing original vs condensed (output: SEMANTIC LOSS / AMBIGUITY / FALSE POSITIVE per finding with verbatim citations) → revert every SEMANTIC LOSS + AMBIGUITY → run `markdownlint-cli2` → ship or revert.

## Backend selection

Default-action Step B picks the mechanical-compression backend: the `caveman` plugin (marketplace `caveman`, invoked as `/caveman:compress`) when present, otherwise the in-session Edit-based fallback. Caveman performs the mechanical flavor cuts (articles, fillers, hedging, verbose-verb collapses) as the compression backend — it is NOT the verification gate. Fallback policy is graceful: the in-session Edit-based path substitutes whenever caveman is absent or unwanted. Subsequent steps (semantic-diff dispatch, revert pass, markdownlint) wrap the output regardless of backend choice.

`disable-model-invocation: false` is deliberate: compress is model-invocable with interview confirmation gates and permission-governed Edit/Bash; not an oversight relative to D1 guidance that mutating skills often set the flag true.

Note the distinction inside that plugin: `/caveman:compress` is a function-call skill (this skill's backend); `/caveman:caveman` is a session-wide response formatter — unrelated to this skill.

**Step A — detect caveman plugin:** `bash "${CLAUDE_SKILL_DIR}/scripts/detect-caveman.sh"`
Tri-state: `available` → prefer caveman; `absent` OR `unknown` → treat as absent and use the Edit fallback (`unknown` means `claude`/`jq` missing from PATH — fail open to Edit, not a hard error).

**Step B — caveman backend (preferred):**

```bash
tempdir=$(mktemp -d)
trap 'rm -rf "$tempdir"' EXIT
cp "$target" "$tempdir/$(basename "$target")"
# Invoke caveman via Skill tool on tempdir copy:
# Skill(caveman:compress, args="$tempdir/$(basename "$target")")
# Caveman writes compressed output to tempdir/basename and backup to tempdir/<basename>.original.md.
# Both stay inside tempdir; trap cleans on EXIT.
cp "$tempdir/$(basename "$target")" "$target" # only on caveman success
```
**Step B — caveman backend (preferred when available):** cross-tool-call steps (Bash state does not persist across tool calls — no `trap … EXIT`, no relying on `$tempdir` in a later call):

1. **Bash call 1** — create a temp copy and echo its absolute path (no EXIT trap):
```bash

Check failure on line 37 in plugins/docs-hygiene/skills/compress/SKILL.md

View workflow job for this annotation

GitHub Actions / hygiene

Fenced code blocks should be surrounded by blank lines [Context: "```bash"]
tempdir=$(mktemp -d)
cp "$target" "$tempdir/$(basename "$target")"
printf '%s\n' "$tempdir/$(basename "$target")"
```

Check failure on line 41 in plugins/docs-hygiene/skills/compress/SKILL.md

View workflow job for this annotation

GitHub Actions / hygiene

Fenced code blocks should be surrounded by blank lines [Context: "```"]
2. **Skill call** — `Skill(caveman:compress, args="<absolute-path-from-step-1>")` on that temp copy. Caveman may write `<file>.original.md` beside the copy inside the tempdir.
3. **Bash call 2** — on caveman success, copy the compressed file back and remove the tempdir explicitly:
```bash

Check failure on line 44 in plugins/docs-hygiene/skills/compress/SKILL.md

View workflow job for this annotation

GitHub Actions / hygiene

Fenced code blocks should be surrounded by blank lines [Context: "```bash"]
cp "<absolute-path-from-step-1>" "$target"
rm -rf "$(dirname "<absolute-path-from-step-1>")"
```

Check failure on line 47 in plugins/docs-hygiene/skills/compress/SKILL.md

View workflow job for this annotation

GitHub Actions / hygiene

Fenced code blocks should be surrounded by blank lines [Context: "```"]
On caveman failure, skip the `cp` and still `rm -rf` the tempdir so the real target is untouched.

Tempdir wrapper contains caveman's hardcoded `<file>.original.md` backup write. Real-path file replaced atomically on success. Consumers may add a defensive `**/*.original.md` entry to their `.gitignore` as belt-and-suspenders against tempdir cleanup races or future caveman backup-path-convention changes.
Tempdir wrapper contains caveman's hardcoded `<file>.original.md` backup write. Real-path file replaced only on success. Consumers may add a defensive `**/*.original.md` entry to their `.gitignore` as belt-and-suspenders against cleanup races or future caveman backup-path-convention changes.

**Step B fallback — in-session Edit (caveman absent or disabled):**
**Step B fallback — in-session Edit (caveman absent, unknown, or unwanted):**

Agent applies Edit ops directly on `$target` per the `context/flavor-vs-content-matrix.md` taxonomy. Same flavor-vs-content rules; no backend indirection.
Agent applies Edit ops directly on `$target` per the `context/flavor-vs-content-matrix.md` taxonomy (full matrix, including restatement deletion). Same flavor-vs-content rules; no backend indirection.

**Step C+ unchanged:** semantic-diff dispatch (mandatory hard rule), revert pass for SEMANTIC LOSS / AMBIGUITY / UNCERTAIN findings, markdownlint-cli2, summary.

Expand All @@ -56,7 +60,7 @@
| Action | Args | Behavior |
|---|---|---|
| `<target>` (default, no action keyword) | empty → uncommitted `.md` from `git status`; file path → single-file; dir path → batch | snapshot → backend → dispatch → revert-pass → markdownlint verify → summary |
| `audit [target]` | same target rules | read-only dry-run; compute expected-yield heuristic per `context/target-types.md`; classify SKIP/COMPRESS/UNCERTAIN |
| `audit [target]` | same target rules | read-only dry-run; run `scripts/audit-scan.sh` (six-signal heuristic in `context/target-types.md`); classify SKIP/COMPRESS/UNCERTAIN |

Flags (apply to both actions):

Expand All @@ -80,7 +84,7 @@
1. **Offer** (AskUserQuestion): run against all tracked eligible `.md` files? Decline → no-op exit.
2. **Audit first** (free — mechanical scan, no subagents): run the audit action over every tracked eligible `.md`. Present INLINE only aggregate counts per class, a dispatch-cost estimate (2 subagent requests per compressed file), and a top-20 excerpt of COMPRESS rows selected deterministically: expected-yield band descending, then word count descending, then lexical path (band strings tie; the two tie-breaks keep the excerpt stable run-to-run). Write the full per-file table to a file — destination `${CLAUDE_PLUGIN_DATA}/audit/<branch-or-scope>-audit.md` when that dir is writable, otherwise a temp path echoed to the user — lexically sorted per the "Summary output deterministic" hard rule — and point at it. Never render every row inline — on a large repo the full table can run to hundreds of KB and truncate the confirmation prompt it feeds. **Stop here when the invocation was the audit action** (report-only).
3. **Interview with prescribed defaults** (AskUserQuestion, recommended option listed first) — default (mutating) action only:
- **Scope** — default: all COMPRESS-classified files, highest expected yield first; alternates: top-N highest-yield subset, include UNCERTAIN, stop after audit (report only).
- **Scope** — default: **top-10** COMPRESS-classified files, highest expected yield first (report-only / decline remains available); alternates: top-N (user picks N), all COMPRESS, include UNCERTAIN, stop after audit (report only). Downgraded from "all COMPRESS" after the 2026-08-15 calibration run (87 consecutive auto-reverts) — see `context/fan-out-orchestration.md` circuit breaker.
- **Concurrency** — default: 2 concurrent subagents per wave (rate-limit-conservative); alternates: 1 (sequential), 3-5 (`context/fan-out-orchestration.md` default).
- **Always-loaded files** — default: excluded (SKIP per the 2-3% empirical baseline); including them requires the same explicit opt-in as `--force`.
4. **Confirm and run**: batch default action over the confirmed set, waves per `context/fan-out-orchestration.md`. Every per-file hard rule — semantic-diff dispatch, revert pass, markdownlint, `<3% AND 0 semantic-loss → REVERT` — applies unchanged.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Compress fan-out orchestration

Read this when batch-compressing N markdown files via parallel subagents. Codifies the multi-phase split that keeps the mandatory semantic-diff in a SEPARATE fresh-context auditor. Before Claude Code v2.1.172 a subagent could not spawn the verifier at all (no nested Agent tool); as of v2.1.172 a foreground subagent can, but nested spawning is version-dependent and a fresh-context verifier beats self-critique regardless — so the auditor phase stays a main-session dispatch.
Read this when batch-compressing N markdown files via parallel subagents. Codifies the multi-phase split that keeps the mandatory semantic-diff in a SEPARATE fresh-context auditor. Nested subagent spawning has been version- and settings-dependent since v2.1.172 (defaults have moved across releases); a fresh-context verifier beats self-critique regardless — so the auditor phase stays a main-session dispatch.

**Why this exists:** `/docs-hygiene:compress` "Hard rules" mandate semantic-diff dispatch. A subagent that invokes `/docs-hygiene:compress` must NOT run that dispatch as a self-audit in its own context — self-audit by the same model that produced the edits drifts toward EXPANSION ("preserve clarity" re-adds words just removed; an observed failure — see ## History). Fix: move the semantic-diff into a separate fresh-context subagent dispatched by the main session.

Expand All @@ -18,6 +18,7 @@ Compress exactly ONE file: <ABSOLUTE-PATH>
LATITUDE:
- Mechanical drops: articles (the/a/an) before clear nouns, filler (just/really/basically/actually/simply), hedging (perhaps/somewhat/might in factually-direct statements), pleasantries, verbose verb phrases (in order to → to, due to the fact that → because, make use of → use)
- Prose playbook: passive → active voice, nominalization collapse ("performs analysis of" → "analyzes", "is responsible for" → "owns")
- Batch fan-out does NOT delete sentence-level restatements (that latitude is Edit-fallback / single-file only — see SKILL.md Purpose).

HARD RULES:
- NEVER add words. EVER.
Expand Down Expand Up @@ -67,6 +68,7 @@ Per FINDING block returned in Phase B:
- **Phase A scope fence** — each compressor subagent's prompt names exactly ONE allowed file; any other file, git operation, or path is forbidden (the template above encodes this)
- **Phase A does NOT invoke `/docs-hygiene:compress`** as a slash command from subagents — self-audit in the compressor context caused reverse-direction edits (see ## History)
- **Refuse-fast threshold** — 5 consecutive Phase A or Phase B ERROR returns aborts the batch
- **Yield circuit breaker** — 5 consecutive auto-reverts in a wave (sub-3% / 0-SL successful outcomes that still discard the edit) → pause, report observed yield, and re-confirm with the user before the next wave. Reverts are not ERRORs; without this breaker a misclassified COMPRESS cohort burns two Opus dispatches per file to completion (2026-08-15 calibration: 87 consecutive auto-reverts).
- **Phase B returns are unverified synthesis** — the main session reverts per finding rather than verifying each by hand; a forbidden citation token invalidates the whole dispatch

## History
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Canonical FLAVOR / CONTENT taxonomy for the `/docs-hygiene:compress` semantic-di
- Filler (just/really/basically/actually/simply)
- Hedging (perhaps/somewhat/might)
- Pleasantries
- Redundant restatement of bold rule names
- Redundant restatement of bold rule names (single-file Edit fallback only — batch Phase A LATITUDE does not delete sentences)
- "in order to" / "due to the fact that" verbose forms
- Conversational connectives ("that said", "in other words")
- Conversational connectives ("that said", "in other words") (Edit fallback; batch LATITUDE is word-level)
- Verbose verb phrases ("make use of" → "use")

### Content (NEVER cut)
Expand All @@ -37,7 +37,7 @@ The taxonomy is invariant across content types. What varies is the EXPECTED YIEL
| **Always-loaded instruction file** (`.claude/rules/**`, `AGENTS.md`, `CLAUDE.md`, `**/SKILL.md`) | 2-3% | (a) directives, (d) scope qualifiers, (e) rule-unique rationale, (f) cross-references | Author-time-disciplined. Default action will revert per SKILL.md "Hard rules" (<3% AND 0SL → REVERT). `--force` only when a targeted sub-3% diff is intentional. Empirical baseline: 3/3 attempts reverted |
| **Onboarding doc** (README onboarding, `docs/onboarding-*.md`, contributor guides) | 8-15% | (b) prohibited-pattern tokens, (c) counter-examples, (h) thresholds | Verbose-prose baseline. Hedging + pleasantries dense; restatement of policy across sections common. Revert-pass strictness: keep every "X not Y" pair intact (counter-example loss = ambiguity in onboarding) |
| **README** (`README.md`, `*/README.md` at app/lib/service roots) | 5-12% | (f) cross-references, (g) exception clauses, (j) inline-code tokens | Project-front-door surface. Inline-code density usually high (commands, paths); revert any (j) drop. Cross-references load-bearing for navigation |
| **Drifted skill body** (`**/SKILL.md` past ~250 lines AND not author-time-disciplined) | 4-7% | (a) directives, (e) rule-unique rationale, (i) enumeration items | Skill bodies tend to accumulate procedural prose during evolution. Revert any directive softening ("must" → "should"); revert any enumeration-item drop. Often a single revert-pass produces a final ship |
| **Drifted skill body** (`**/SKILL.md` past ~250 lines AND not author-time-disciplined) | 4-7% (Edit-fallback / explicit target only) | (a) directives, (e) rule-unique rationale, (i) enumeration items | **Unreachable via the audit gate's batch path:** signal 1 unconditionally SKIPs every `**/SKILL.md`. Drifted skill bodies require an explicitly-named single-file target (or Edit fallback); the matrix row remains for that niche. Revert any directive softening ("must" → "should"); revert any enumeration-item drop |
| **Third-party pasted prose** (vendor docs, external policy text, copied research notes) | 10-20% | (b) prohibited-pattern tokens, (h) thresholds, (j) inline-code tokens | Highest yield + highest risk. Pasted prose carries verbose flavor authors did not edit. Inline-code tokens (CLI flags, schema field names) MUST survive verbatim; treat any (j) loss as SEMANTIC LOSS not AMBIGUITY |

## Variants never relax the preservation contract
Expand All @@ -49,8 +49,8 @@ The (a)–(j) Content list defines the universal preservation contract. Per-cont
`/docs-hygiene:compress audit <target>` classifies SKIP / COMPRESS / UNCERTAIN per `context/target-types.md` "Author-time-signal heuristic". The "Expected yield" column above feeds that heuristic's output:

- Expected yield < 3% (always-loaded instruction files) → audit emits **SKIP** with empirical-baseline citation
- Expected yield 3-7% (drifted skill bodies) → audit emits **UNCERTAIN**; user gates via `--force` or skip
- Expected yield ≥ 8% (onboarding / README / third-party) → audit emits **COMPRESS**
- Expected yield 3-7% (density-narrow files, or an explicitly-targeted drifted skill body) → audit emits **UNCERTAIN**; user gates via `--force` or skip. Note: `**/SKILL.md` never reaches this band through the mechanical audit gate (signal 1 wins).
- Expected yield ≥ 8% (onboarding / README / third-party) → audit emits **COMPRESS**; the matrix's 8-15% / 10-20% bands assume Edit-fallback latitude on restatement-heavy prose and over-predict batch fan-out yield

Numeric ranges drift; revisit the variant table as empirical evidence accumulates.

Expand Down
Loading
Loading