diff --git a/plugins/disk-hygiene/.claude-plugin/plugin.json b/plugins/disk-hygiene/.claude-plugin/plugin.json index 21ab1e1331..b98838474a 100644 --- a/plugins/disk-hygiene/.claude-plugin/plugin.json +++ b/plugins/disk-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "disk-hygiene", - "version": "0.13.0", + "version": "0.15.0", "description": "Context-aware disk hygiene for arbitrary directory trees: inventories orphaned and temporary artifacts, classifies evidence into review tiers, and offers exact-path cleanup only after a fresh safety preview and explicit per-tier approval. The target is read-only by default; OS-managed paths, links and mount points, VCS-tracked content, changed entries, and live-handle uncertainty fail closed.", "author": { "name": "Melodic Software", diff --git a/plugins/disk-hygiene/CHANGELOG.md b/plugins/disk-hygiene/CHANGELOG.md index 659f8a12dc..bf668c1da7 100644 --- a/plugins/disk-hygiene/CHANGELOG.md +++ b/plugins/disk-hygiene/CHANGELOG.md @@ -3,6 +3,46 @@ All notable changes to the `disk-hygiene` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.15.0] + +> Version note: `0.14.0` is claimed by PR #1870, open against this manifest. This entry takes the +> next number so the two do not collide. + +### Fixed + +- **A confirmation question is unanswerable when its acceptance bar names something the question + never showed.** The gate applied one bar — "an affirmative answer naming exactly the tier and path + list just shown" — to every question the skill asks, including the no-target prompt and §1's + large-scan confirmation. Neither has presented a tier or a path list, so no reply a human could + give satisfied the stated bar, and the two questions the gate exists to protect were the only ones + it could actually be cleared for. The question surface rule and the answer floor (the user's own + answer, this session, never inferred, stop on rejection) stay common to all four questions; what an + answer must *name* is now stated per question — a directory for target selection, the target plus a + deliberate unbounded walk for scan scope, the exact tier and path list for removal and the manual + handoff. §1's and §6's cross-references now name their row instead of asserting the deletion bar + applies unchanged, and the gate states the obligation that generated the defect: ask each question + so it shows what its row requires the answer to name. + +- **The confirmation gate fell back to an inline question only when `AskUserQuestion` was + *absent*.** Permission mode `dontAsk` "auto-denies tools unless pre-approved … `AskUserQuestion` … + denied even if you've allowed them" + ([permissions](https://code.claude.com/docs/en/permissions), fetched 2026-08-08), which leaves the + tool visible in the pool while every call fails; only a bare-name deny rule "removes the tool from + Claude's context entirely". Absence and denial are therefore distinct states, and keying the + fallback on absence let a `dontAsk` session pick a tool it cannot use and leave the destructive + confirmation gate unsatisfied rather than asking inline. The fallback now triggers on absent, + denied, **or otherwise unusable** — including a denial discovered only by calling it — so a state + neither named case anticipates still routes to the inline question. + +- **The `python3` alias probe could not be reached on a machine whose only alternate interpreter + cannot run it.** `setup` step 1(b) classifies the `python3` resolution with a bundled inspect-only + probe launched through some other interpreter, and routed to the PowerShell equivalent only when no + such interpreter existed at all. A real-but-incompatible launcher — Python 3.6, which the same + section already names as an interpreter that rejects `from __future__ import annotations`, or a + legacy `python` 2.x — is not absent, so the check had no path to a verdict and could classify + neither the Store stub nor its own remediation. The PowerShell fallback now also covers a chosen + interpreter that emits no verdict. + ## [0.13.0] > Version note: `0.11.0` is claimed by #1804 (PR #1818) and `0.12.0` by #1805 (PR #1819), both open diff --git a/plugins/disk-hygiene/skills/clean/SKILL.md b/plugins/disk-hygiene/skills/clean/SKILL.md index e0a6ee43a0..8573e349bf 100644 --- a/plugins/disk-hygiene/skills/clean/SKILL.md +++ b/plugins/disk-hygiene/skills/clean/SKILL.md @@ -72,20 +72,32 @@ unless bounded with `--max-depth` or confirmed with `--confirmed-large-scan`. ## Confirmation gate -Every question this skill asks passes this gate — including the no-target prompt above, the -large-scan confirmation in §1, the removal approval in §5, and the unsupported-platform handoff -in §6. +Every question this skill asks passes this gate — the no-target prompt above, the large-scan +confirmation in §1, the removal approval in §5, and the unsupported-platform handoff in §6. One +surface rule and one floor cover all four. What a valid answer must *name* is per question, because +a target prompt has no tier or path list to name and cannot be held to a bar built for one. **Question surface.** Prefer `AskUserQuestion`: its answer is the user's own and cannot be -fabricated. It is not always in the pool — permission mode `dontAsk` denies it unconditionally, and -a bare-name `permissions.deny` rule or a `disallowed-tools` entry removes it — so when it is absent, -ask the same question inline as a numbered choice and wait for the reply. The surface varies; the bar -below does not. - -**The bar.** Take the user's own affirmative answer, given in this interactive session, naming -exactly the tier and path list just shown. A prior general request, `--execute`, "clean everything", -approval of another tier, or silence is not confirmation — never supply or infer the answer -yourself. On rejection, stop. +fabricated. It is not always usable, in two distinct ways — a bare-name `permissions.deny` rule or a +`disallowed-tools` entry removes it from context entirely, while permission mode `dontAsk` denies it +even when an allow rule names it, leaving it visible and every call failing. Fall back to the same +question asked inline as a numbered choice whenever the tool is absent, denied, **or otherwise +unusable** — including a denial discovered only by calling it; a denied call is an unanswered +question, never an answer. Then wait for the reply. + +**The floor — every question.** Take the user's own answer, given in this interactive session. Never +supply, infer, or fabricate it: a prior general request, `--execute`, "clean everything", approval of +another tier, or silence is not an answer. On rejection, stop. + +**What the answer must name — per question.** Where a row requires the answer to name something the +skill itself produced — the resolved target, the tier, the path list — show it in the question; a bar +naming what the question never presented cannot be met. + +| Question | Accept only an answer naming | +|---|---| +| Target selection (no target given) | one directory, which must then clear every rejection in "Arguments and boundaries" | +| Scan scope (`--confirmed-large-scan`, §1) | that target and a deliberate unbounded full walk of it | +| Removal approval (§5) and manual handoff (§6) | exactly the one tier and the exact path list just shown | ## 1. Create a read-only snapshot @@ -116,8 +128,9 @@ this gate) and carries neither `--max-depth` nor `--confirmed-large-scan` return unbounded traversal, so a forgotten bound never becomes an accidental whole-volume scan. `--max-depth` is the preferred bounded response. Reserve `--confirmed-large-scan` for a deliberate full walk the human has confirmed — pass the -[confirmation gate](#confirmation-gate) first, exactly as the apply lane requires before an -expensive step; a general "clean my home directory" is not that confirmation. Every +[confirmation gate](#confirmation-gate)'s scan-scope row first, the same standing before an +expensive step that the apply lane demands before a destructive one; a general "clean my home +directory" is not that confirmation. Every directory whose descendants were not walked — cut off by `--max-depth`, a protected root, or a VCS boundary — is recorded in `truncated_paths`; report them as coverage gaps, never as clean, and never plan them for removal (the preview blocks them as `truncated-not-inventoried` and skips the @@ -249,10 +262,12 @@ token. Preview reports `execution-platform-unsupported` as a per-candidate blocker on these platforms, so the engine never deletes there. The default outcome is the report. The manual lane is gated by `--execute` exactly as the engine lane is — without it, no deletion lane may be offered on any -platform. If — and only if — `--execute` was requested and the human reviews the report and -approves an exact path list in this interactive session (the same [confirmation gate](#confirmation-gate) -exact-tier-and-list bar as the engine lane; a general "clean it up" is still not approval), removal is a manual -handoff, not an engine plan: +platform. If — and only if — `--execute` was requested and the human reviews the report and approves +an exact path list drawn from one tier in this interactive session (the §3 report spans every tier, so +narrow it to a single tier and show that tier's paths before asking — the +[confirmation gate](#confirmation-gate)'s removal row is the same exact-tier-and-list bar the engine +lane clears; a general "clean it up" is still not approval), removal is a manual handoff, not an +engine plan: 1. Write the approved exact paths to `/handoff-paths.json` as `{"version": 1, "paths": ["relative/exact.tmp"]}` (snapshot-relative, exact, non-overlapping, diff --git a/plugins/disk-hygiene/skills/setup/SKILL.md b/plugins/disk-hygiene/skills/setup/SKILL.md index b4dfc5451b..28ed5675b7 100644 --- a/plugins/disk-hygiene/skills/setup/SKILL.md +++ b/plugins/disk-hygiene/skills/setup/SKILL.md @@ -64,8 +64,12 @@ fails closed like every other guard-relevant unknown in this plugin. `python3` (`py -3`, `python`, or an absolute interpreter path — any interpreter already proven real): `"" "${CLAUDE_PLUGIN_ROOT}/skills/setup/scripts/python3_alias_probe.py"`; if no - such interpreter exists, apply the probe's own portable signal directly in PowerShell — a - zero-length file under a `WindowsApps` path component is the stub + such interpreter exists **or the one you chose emits no JSON verdict**, apply the probe's own + portable signal directly in PowerShell. Proven real is not the same as able to run the probe: + a pre-3.7 interpreter rejects its `from __future__ import annotations` and a legacy `python` + 2.x fails earlier still, each before anything is classified — so a machine whose only + alternate launcher is Python 3.6 reaches the verdict through PowerShell, not by having no + launcher at all. The signal: a zero-length file under a `WindowsApps` path component is the stub (`(Get-Item -Force (Get-Command python3).Source)` → `Length` 0 plus a `ReparsePoint` attribute); (c) only after the verdict is `ok` may the version probe execute `python3` — and distinguish its two failure modes for the remediation wording: an interpreter that