13 KiB
doc-clean Specification
Purpose
TBD - created by archiving change add-clean. Update Purpose after archive.
Requirements
Requirement: Per-File Transaction with Content-Hash Guard
The patch applier SHALL apply all entries for a given file as a single in-memory
transaction. It SHALL read the file's bytes once, verify that sha256(bytes)
matches expected_sha256 for every anchored entry on that file, and on any
mismatch skip the entire file batch with reason content-changed-since-check
(recommending re-analysis) while continuing to process other files. It SHALL apply
anchored edits in memory in descending order by anchor.start_line, and SHALL apply
insert-frontmatter last (after all anchor edits) because it prepends and shifts
line numbers. It SHALL write the file once and stage once after all in-memory edits
succeed.
Scenario: Mismatch skips the entire file batch
- WHEN a file's current sha256 differs from any anchored entry's
expected_sha256 - THEN the applier skips the entire file batch with reason
content-changed-since-check, leaves the file untouched, and continues processing other files
Scenario: Descending-anchor application preserves line offsets
- WHEN a file has multiple anchored entries (e.g., line 40 and line 10)
- THEN the applier applies the line-40 edit first, then the line-10 edit, so the second edit lands on the correct line in the still-unmodified earlier portion of the file
Scenario: insert-frontmatter is applied last
- WHEN a file has both an anchored edit and an insert-frontmatter entry
- THEN the anchored edit is applied first in memory, and insert-frontmatter is appended last (after all anchor edits) before the single file write
Scenario: Write and stage occur once per file
- WHEN a file has multiple entries applied in memory
- THEN the file is written to disk exactly once and staged exactly once after all in-memory edits succeed
Requirement: insert-frontmatter Re-Derives Freshness at Apply Time
The patch applier SHALL re-derive frontmatter state at apply time for
insert-frontmatter entries (which carry no expected_sha256 because they have no
anchor) by re-reading the file and parsing its frontmatter. If the target key is
already present with the target value, the applier SHALL treat the operation as an
idempotent no-op (success, no write). If the target key is present with a different
value, the applier SHALL skip the entry with reason frontmatter-key-conflict and
SHALL NOT overwrite the existing value. If the key is absent, the applier SHALL
insert key: value (creating a --- block if none exists) as the last in-memory
step.
Scenario: Key already present with target value — idempotent no-op
- WHEN the applier processes an insert-frontmatter entry and the file already contains
hygiene: frozen - THEN the applier treats the entry as a success and does not modify the file
Scenario: Key present with conflicting value — skip
- WHEN the applier processes an insert-frontmatter entry and the file already contains
hygiene: review - THEN the applier skips the entry with reason
frontmatter-key-conflictand does not overwrite the existing value
Scenario: Key absent — insert
- WHEN the applier processes an insert-frontmatter entry and the file has no
hygienefrontmatter key - THEN the applier inserts the key-value pair (creating a
---block if needed) as the last in-memory step before writing
Requirement: Incompatible-Ops Detection Skips the File
The patch applier SHALL detect incompatible-op combinations on a per-file basis
before performing any edits. If move-to-archive co-occurs with any other op on the
same file, or if any anchor ranges overlap, the applier SHALL skip the entire file
with reason incompatible-ops-on-file and SHALL recommend re-analysis. The applier
SHALL NOT attempt to compose or sequence incompatible ops in v1.
Scenario: move-to-archive with another op — skip
- WHEN a file has both a move-to-archive entry and a replace-text entry
- THEN the applier skips the entire file with reason
incompatible-ops-on-fileand records a re-analysis recommendation
Scenario: Overlapping anchors — skip
- WHEN two entries on the same file have anchor ranges that overlap (e.g., lines 5–15 and lines 10–20)
- THEN the applier skips the entire file with reason
incompatible-ops-on-file
Scenario: Non-conflicting entries on a file proceed normally
- WHEN a file has two entries with non-overlapping anchors and no move-to-archive
- THEN the applier applies both entries via the descending-anchor transaction
Requirement: Safety-Tier Gating Before Any Mutation
The clean skill SHALL partition report entries by safety tier (read from the report —
never recomputed) into auto entries (applied without prompt) and confirm entries
(escalated before any mutation). It SHALL present all confirm-tier entries as a
single batch-confirm list showing path, category, op, token count, and rationale with
per-entry opt-out, visually distinguishing irreversible delete-range entries from
reversible entries. The approved set SHALL be all auto entries plus any
user-approved confirm entries. The gate SHALL run identically under sweep — the
/hygiene sweep convenience path SHALL NOT bypass invariant #7.
Scenario: auto entries apply without prompt
- WHEN the report contains only auto-tier entries (move-to-archive, insert-frontmatter, replace-text, dedupe)
- THEN the clean skill applies them without presenting a confirm prompt
Scenario: confirm entries escalate before any mutation
- WHEN the report contains confirm-tier entries (delete-range or any generative op)
- THEN the clean skill presents a batch-confirm list before any file is modified, and applies only user-approved entries
Scenario: per-entry opt-out is respected
- WHEN the user approves some confirm entries and opts out of others
- THEN the skill applies the approved entries and skips the opted-out entries
Scenario: sweep does not bypass the gate
- WHEN the user runs /hygiene sweep and the report contains confirm-tier entries
- THEN the confirm gate runs identically to a standalone /hygiene clean
Requirement: Git-Safe Single Commit
The clean skill SHALL produce exactly one git commit per run. Before any mutation it
SHALL resolve the project root via StateStore, run git status --porcelain, and
if the tree is dirty, SHALL automatically create a WIP checkpoint commit of the
user's work before proceeding, so the cleanup commit remains exactly one. The cleanup
commit SHALL be created via git-context commit-apply --message-stdin with a
generated message summarizing auto/confirmed/skipped counts and op breakdown.
Staging SHALL be precise: for non-move ops the skill calls git add <staged_paths>
from the applier result; for move-to-archive the applier calls git mv (staging
both sides) and the skill SHALL NOT git add the destination path again. The skill
SHALL NEVER use git add -A or git add .. If a hard failure occurs (applier
exit 2, git mv fail, or write error), the skill SHALL roll back via
git restore/reset to the pre-run baseline and abort with a structured error.
Partial success (some file batches guard-skipped) SHALL NOT trigger rollback — the
skill SHALL commit what applied and report skipped files. Untracked candidate docs
SHALL be skipped and reported (tracked-files-only). last_clean SHALL be stamped
to the commit instant, not the run-start instant.
Scenario: Clean tree produces exactly one commit
- WHEN the user runs /hygiene clean with a clean git tree
- THEN the run produces exactly one git commit containing all applied edits
Scenario: Dirty tree gets a WIP checkpoint then one cleanup commit
- WHEN the user runs /hygiene clean with unstaged changes in the working tree
- THEN the skill auto-creates a WIP checkpoint commit of the user's work, then produces exactly one cleanup commit — two commits total, cleanup still exactly one
Scenario: All-skipped produces zero commits
- WHEN all entries are guard-skipped (every file changed since check)
- THEN the skill produces zero commits and reports all files as skipped with re-analysis recommended
Scenario: Hard failure triggers rollback
- WHEN a write error or applier exit 2 occurs mid-run
- THEN the skill rolls back to the pre-run baseline and aborts with a structured error; no partial commit is created
Scenario: Partial success commits what applied
- WHEN some file batches are guard-skipped (applier exit 1) and others succeed
- THEN the skill commits the applied edits and reports the skipped files, without rolling back the applied changes
Scenario: move-to-archive is not double-staged
- WHEN the applier stages a move-to-archive via git mv (both source and dest)
- THEN the skill does not call git add on the destination path again
Requirement: Clean Skill Orchestration
The hygiene-clean skill SHALL load the current report via StateStore.read_report
(if none, it SHALL tell the user to run /hygiene check and stop). It SHALL
re-validate the loaded report via validate_report.py (if invalid, stop). It SHALL
apply a scope/category filter to entries. It SHALL partition entries into
auto+deterministic, confirm+deterministic (i.e., delete-range), and
generative (always confirm) subsets. It SHALL gate confirm entries before any
mutation. It SHALL run the git preflight (clean check / WIP checkpoint). It SHALL
apply deterministic approved entries via patch_applier.py. It SHALL dispatch
generative approved entries to a Sonnet subagent via workflows/distill.md
(LOOP-GUARD subagent pointer). It SHALL stage precisely and commit once via
git-context commit-apply --message-stdin. It SHALL stamp last_clean. It SHALL
surface applied/skipped (with re-analysis notes) / confirmed counts and the commit
SHA.
Scenario: No report — prompt to check first
- WHEN the user runs /hygiene clean and no report exists in .dochygiene/
- THEN the skill tells the user to run /hygiene check first and stops without modifying anything
Scenario: Invalid report — stop
- WHEN the loaded report fails re-validation
- THEN the skill stops and reports the validation error without modifying anything
Scenario: Zero in-scope entries — no-op report
- WHEN all entries are filtered out by scope/category
- THEN the skill reports zero in-scope entries and exits without modifying the tree or creating a commit
Scenario: Full pipeline succeeds
- WHEN the report contains valid entries and the user approves all confirm entries
- THEN the skill applies deterministic entries via the applier, dispatches generative entries to Sonnet, stages precisely, commits once, stamps last_clean, and surfaces the commit SHA
Requirement: Generative Distillation via Live-Read Sonnet Subagent
For approved generative entries, the clean skill SHALL confirm the file still exists
(a preceding move-to-archive on the same file would have removed it). It SHALL read
the live file contents at dispatch time (not from the report cache) to guarantee
freshness, because generative entries carry no expected_sha256. It SHALL dispatch
to a Sonnet subagent pointing at workflows/distill.md (LOOP-GUARD — the subagent
reads that workflow, not SKILL.md, to avoid recursion). The subagent SHALL return
new prose (or new-primary + archived-section for split). The skill SHALL write and
stage the result. A file with both a generative entry and a deterministic entry SHALL
be treated as incompatible-ops-on-file and skipped.
Scenario: File missing at dispatch time — skip
- WHEN a generative entry targets a file that no longer exists (e.g., moved by a prior step)
- THEN the skill skips the generative entry and reports it as skipped
Scenario: Live read ensures freshness
- WHEN the skill dispatches a generative entry to Sonnet
- THEN it reads the file's current bytes immediately before dispatch, not from the cached report span
Scenario: Generative + deterministic on same file — incompatible
- WHEN a file has both a generative entry and a deterministic entry
- THEN the applier (or skill) skips the file with reason incompatible-ops-on-file and recommends re-analysis
Requirement: Sweep Composes Check Then Clean at Command Level
/hygiene sweep SHALL invoke the hygiene-check skill followed by the
hygiene-clean skill, passing --scope and --category to both. The sweep SHALL
NOT produce a double-commit: check writes only to the gitignored .dochygiene/
directory and never commits; the single cleanup commit from clean is the only git
commit the sweep produces. The sweep routing SHALL live in commands/hygiene.md,
not in a third skill or by having the clean skill invoke the check skill internally.
Scenario: Sweep routes through the command, not a third skill
- WHEN the user runs /hygiene sweep
- THEN commands/hygiene.md invokes hygiene-check then hygiene-clean sequentially, passing shared flags
Scenario: Sweep produces at most one cleanup commit
- WHEN the user runs /hygiene sweep on a clean tree
- THEN at most one git commit is produced (the cleanup commit from clean); check writes nothing to git