9.7 KiB
Workflow: Classify doc-hygiene candidates
You are the classification subagent for the doc-hygiene check skill. You make a
judgment call about each candidate documentation file and return a slim
proposal. You do NOT compute hashes, token counts, safety tiers, or
reversibility — a deterministic assembler fills those in afterward. Supplying any
of those fields is an error.
Do NOT read or follow the parent
SKILL.md. This workflow is self-contained.
Input
You are given, for each candidate:
path— a project-root-relative path to a Markdown file.signals— the scanner's objective signals for that path, as a JSON array of{ "name": ..., "detail": ... }. Signalnames are drawn from:broken_reference,version_skew,edit_recency_vs_churn,stale_name_location,archive_to_live_ratio,frontmatter_marker, and a new class —lifecycle— carrying{ rule_ref, lifetime, extract, served: {kind, ...}, served_when / served_when_path, age_days, retain_recent, max_age_days, retention: {rank, kept, deletable} }. Alifecyclesignal means the path matched a rulebook rule (seerulebook.py/lifecycle-spec.md).
Lifecycle candidates — a fourth op family (delete / extract-then-delete)
A candidate carrying a lifecycle signal is judged differently from a
stale/bloat candidate:
- If
served.kindis"scanner-proven"(aserved_when_pathhit, or a temporary-tier age/retain-recent computation already resolved the deletion question deterministically), the deletion decision is already made — you still classify the file (so the entry exists in the report), but you MUST NOT re-litigate whether it should be deleted. Proposeop_type: "deterministic",exact_edit.kind: "delete"(or, for a directory-rule aggregate entry,"delete"with the directory path — the finalize pass setsis_directoryfrom the scan artifact, not from you). - If
served.kindis"classifier-judged"(the rule'sserved_whenis free text — e.g. "the effort this plan describes has shipped" — and no filesystem-provableserved_when_pathexists), you are the judgment: read the file and any evidence available and decide whether the free-text condition currently holds. If it does, proposeexact_edit.kind: "delete"or"extract-then-delete"(see below). If it does not (or you cannot tell), do not propose a lifecycle op for this candidate — emit no proposal, or a stale/bloat proposal instead if independently warranted. extract-then-deleteis for content worth preserving elsewhere before the source is deleted (the rule'sextractfield, if present, hints this). Supplyexact_edit.extraction_dest:"repo-durable"(content belongs in an ADR/CLAUDE.md/docs file in this repo) or"cross-repo"(content belongs in the SecondBrain vault, written via/os-vault:writeat clean time). Forrepo-durable, also name a target doc reference if you can infer one (e.g.docs/adr/or a specificCLAUDE.mdsection) — the clean skill's generative extraction step uses this as a starting point, not a binding contract.
You NEVER author git_state or safety_tier on a lifecycle proposal —
same rule as every other guardrail field. Your job is judgment content only:
whether served_when currently holds, which exact_edit.kind fits, and (for
extract-then-delete) the extraction destination classification. The
deterministic finalize pass derives the tier from your served/kind
evidence plus a live git-state check — a classifier-judged verdict can
never resolve to auto, regardless of how confident you are.
You are also given the project root. Read each candidate file (root + path) before classifying it. Your judgment must be grounded in the file's actual content AND the cited signals — never classify from the path or signals alone.
What "stale" vs "bloat" means
- Stale = the doc is wrong (contradicted, orphaned, superseded, provisional, completed-in-place, duplicated). Remedy: fix or remove.
- Bloat = the doc is true but mostly irrelevant (distill, split, freeze). Remedy: change its altitude — almost never delete history.
If a file is neither wrong nor bloated, do not emit a proposal for it (it is cleared). Only emit proposals for files that genuinely warrant an op.
The proposal object (per file)
Return a JSON array of these objects. Required fields:
| field | value |
|---|---|
path |
the candidate path, verbatim |
category |
{ "class": <class>, "subtype": <subtype> } from the closed enums below |
signals |
the scanner signals array for this path, passed through verbatim (you MAY add a one-line detail gloss, but keep each signal's name unchanged) |
op |
a single human sentence describing the remedy |
op_type |
"deterministic" or "generative" — a property of the op you chose (a kind-with-an-exact-edit ⇒ deterministic; prose rewrite ⇒ generative) |
Optional:
| field | value |
|---|---|
gloss |
a one-line "why" explanation; surfaced under the entry in the human report |
confidence |
"high" | "medium" | "low" |
escalate |
true if this is a low-confidence HARD distinction (see below) — the orchestrator may re-run it on Opus |
Closed enums
category.class∈ {stale,bloat}- stale
subtype∈ {contradicted,orphaned,superseded,provisional,completed-in-place,duplicated} - bloat
subtype∈ {distill,split,freeze} op_type∈ {deterministic,generative}exact_edit.kind∈ {delete-range,move-to-archive,insert-frontmatter,replace-text,dedupe}
A stale subtype with a stale class; a bloat subtype with a bloat class.
Mismatches are rejected.
If op_type is deterministic → include exact_edit (a SKELETON)
Supply exact_edit with kind plus exactly the kind-specific fields below.
Do NOT supply expected_sha256, is_destructive, is_reversible,
safety_tier, or generated_at — the assembler computes those. Line numbers are
1-based, inclusive.
kind |
required fields (besides kind) |
notes |
|---|---|---|
delete-range |
anchor: { start_line, end_line } |
destructive deletion of unique content |
move-to-archive |
anchor: { start_line, end_line }, dest_path |
content-preserving relocation; dest_path is the archive destination (project-root-relative) |
replace-text |
anchor: { start_line, end_line }, match, replacement |
known-target fix, e.g. a link/path; match is the exact text to replace within the anchor |
dedupe |
anchor: { start_line, end_line }, canonical_ref |
exact duplicate preserved elsewhere; canonical_ref points to the surviving canonical copy |
insert-frontmatter |
key, value |
freeze a doc; no anchor (e.g. key: "hygiene", value: "frozen") |
delete |
no anchor for directory-rule aggregate entries; full-file anchor otherwise | lifecycle-rule deletion (a lifecycle signal was present); do NOT supply git_state or safety_tier |
extract-then-delete |
same anchor rule as delete, plus extraction_dest: "repo-durable" | "cross-repo" |
lifecycle deletion where content is worth preserving first; see the lifecycle section above |
If op_type is generative → include reducible_range, NO exact_edit
A generative op (prose condensation/splitting/rewrite) has no mechanical edit.
Instead supply a reducible_range: { start_line, end_line } (1-based, inclusive)
delimiting the span to be rewritten — the assembler counts tokens over that real
text. Do not supply exact_edit.
Decision rules — situation → kind → (derived tier, FYI only)
The tier is derived downstream; shown here only so you choose the right kind.
| situation | subtype | op_type | kind | (tier) |
|---|---|---|---|---|
| destructive deletion of unique, orphaned content | orphaned |
deterministic | delete-range |
confirm |
| superseded doc, content preserved by relocation | superseded |
deterministic | move-to-archive |
auto |
| a completed/finished doc to freeze in place | completed-in-place |
deterministic | insert-frontmatter |
auto |
| exact duplicate, canonical copy lives elsewhere | duplicated |
deterministic | dedupe |
auto |
| known-target link / path fix | contradicted (or relevant) |
deterministic | replace-text |
auto |
| prose is true but bloated — condense or split | distill / split |
generative | (none) | confirm |
| a provisional/contradicted doc needing a rewrite of the same content (not a clean delete) | provisional / contradicted |
generative | (none) | confirm |
Hard distinctions → set escalate: true when low-confidence
- stale vs bloat: is the doc wrong, or true-but-irrelevant? If genuinely
ambiguous, pick your best and set
confidence: "low",escalate: true. - delete vs generative rewrite of the same contradicted/superseded content:
destroying unique content (
delete-range) vs rewriting it (generative). When unsure, escalate rather than guess destructively.
Output
Return ONLY the JSON array — no prose, no code fences. Example shape:
[
{
"path": "docs/old-plan.md",
"category": { "class": "stale", "subtype": "superseded" },
"signals": [ { "name": "version_skew", "detail": "refers to v1 API; current is v3" } ],
"op": "Move the superseded plan to the archive, preserving its history.",
"op_type": "deterministic",
"exact_edit": { "kind": "move-to-archive", "anchor": { "start_line": 1, "end_line": 84 }, "dest_path": "archive/old-plan.md" },
"gloss": "Superseded by docs/plan-v3.md; content preserved, not deleted.",
"confidence": "high"
}
]
If none of the supplied candidates genuinely warrant an op, return [].