diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 279af82..f61fa6a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "cozystack-claude-plugins", - "description": "Claude Code plugins for the Cozystack ecosystem — cozystack:* platform skills (wizard, talos-bootstrap, talos-reset, ubuntu-bootstrap, cluster-install, debug, cluster-upgrade, package-deploy, package-bump, external-app-create) plus linstor:* DRBD/LINSTOR operations", + "description": "Claude Code plugins for the Cozystack ecosystem — cozystack:* platform skills (wizard, talos-bootstrap, talos-reset, ubuntu-bootstrap, cluster-install, debug, cluster-upgrade, package-deploy, package-bump, external-app-create, comment-audit, comment-trim) plus linstor:* DRBD/LINSTOR operations", "owner": { "name": "Cozystack", "url": "https://github.com/cozystack" @@ -9,7 +9,7 @@ "plugins": [ { "name": "cozystack", - "description": "Cozystack platform skills bundle — wizard (entry-point orchestrator that interviews + dispatches the chain), talos-bootstrap (Talos node prep via talm with maintenance-mode probe, cert-SAN NAT guardrail, multidoc machine-config, and opt-in boot-method picker), talos-reset (cloud-provider terminate+relaunch helper for unrecoverable Talos nodes — OCI/AWS/GCP/Hetzner; preserves disks + VNICs + NSGs), ubuntu-bootstrap (Ubuntu/Debian k3s bootstrap wrapping ansible-cozystack), cluster-install (Cozystack on a ready cluster — node-readiness, ZFS pool provisioning, extractedprism HA proxy, all-HRs-Ready + storage-pools-registered gate), debug (investigate stuck installs — classify operator-error/config-drift/upstream-bug/not-supported, apply fixes or workarounds, draft upstream issues on approval), cluster-upgrade (release-notes-driven v1.x patch/minor upgrade), package-deploy (dev-loop deploy with ExternalArtifact support), package-bump (single-package version bump with changelog adaptation), external-app-create (scaffold a new external-apps package), dev-ui-bootstrap (Playwright + Vite dev server scaffolding for cozystack-ui pointed at a chosen kubeconfig — worktree, @playwright/test, Chromium, kubectl proxy + Vite on a free port). Invoked as cozystack:wizard, cozystack:talos-bootstrap, cozystack:talos-reset, cozystack:ubuntu-bootstrap, cozystack:cluster-install, cozystack:debug, cozystack:cluster-upgrade, cozystack:package-deploy, cozystack:package-bump, cozystack:external-app-create, cozystack:dev-ui-bootstrap.", + "description": "Cozystack platform skills bundle — wizard (entry-point orchestrator that interviews + dispatches the chain), talos-bootstrap (Talos node prep via talm with maintenance-mode probe, cert-SAN NAT guardrail, multidoc machine-config, and opt-in boot-method picker), talos-reset (cloud-provider terminate+relaunch helper for unrecoverable Talos nodes — OCI/AWS/GCP/Hetzner; preserves disks + VNICs + NSGs), ubuntu-bootstrap (Ubuntu/Debian k3s bootstrap wrapping ansible-cozystack), cluster-install (Cozystack on a ready cluster — node-readiness, ZFS pool provisioning, extractedprism HA proxy, all-HRs-Ready + storage-pools-registered gate), debug (investigate stuck installs — classify operator-error/config-drift/upstream-bug/not-supported, apply fixes or workarounds, draft upstream issues on approval), cluster-upgrade (release-notes-driven v1.x patch/minor upgrade), package-deploy (dev-loop deploy with ExternalArtifact support), package-bump (single-package version bump with changelog adaptation), external-app-create (scaffold a new external-apps package), dev-ui-bootstrap (Playwright + Vite dev server scaffolding for cozystack-ui pointed at a chosen kubeconfig — worktree, @playwright/test, Chromium, kubectl proxy + Vite on a free port), comment-audit (audit code comments in a diff for design-doc prose and report which to cut, which to keep, and which arguments are restated at several sites — reports only), comment-trim (apply the cuts and prove mechanically that only comments changed; leaves the result in the working tree). comment-audit and comment-trim work on any repository, not only Cozystack ones. Invoked as cozystack:wizard, cozystack:talos-bootstrap, cozystack:talos-reset, cozystack:ubuntu-bootstrap, cozystack:cluster-install, cozystack:debug, cozystack:cluster-upgrade, cozystack:package-deploy, cozystack:package-bump, cozystack:external-app-create, cozystack:dev-ui-bootstrap, cozystack:comment-audit, cozystack:comment-trim.", "source": "./plugins/cozystack", "category": "infrastructure" }, diff --git a/CLAUDE.md b/CLAUDE.md index 69ea9f6..81e5bb9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ CLAUDE.md # this file — contributor guidance Two plugins ship today: -- `plugins/cozystack/` — platform bundle (10 skills: wizard, talos-bootstrap, talos-reset, ubuntu-bootstrap, cluster-install, debug, cluster-upgrade, package-deploy, package-bump, external-app-create). +- `plugins/cozystack/` — platform bundle (13 skills: wizard, talos-bootstrap, talos-reset, ubuntu-bootstrap, cluster-install, debug, cluster-upgrade, package-deploy, package-bump, external-app-create, dev-ui-bootstrap, comment-audit, comment-trim). - `plugins/linstor/` — storage-recovery (1 skill: recover). Multi-skill plugin shape: every plugin has one `.claude-plugin/plugin.json` at its root, and one directory per skill under `skills/`. Skills are addressed by Claude Code as `/:` (e.g. `/cozystack:wizard`). diff --git a/README.md b/README.md index 8e42c2d..4ff0701 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Install a plugin: ### cozystack -Platform skills bundle. One install gives you eleven skills, invoked as `/cozystack:`. Start with `/cozystack:wizard` — it asks Talos / Ubuntu / Existing and picks the chain. +Platform skills bundle. One install gives you thirteen skills, invoked as `/cozystack:`. Start with `/cozystack:wizard` — it asks Talos / Ubuntu / Existing and picks the chain. | Skill | Description | | --- | --- | @@ -37,6 +37,8 @@ Platform skills bundle. One install gives you eleven skills, invoked as `/cozyst | **/cozystack:package-bump** | Bump a single package inside the cozystack monorepo — reads upstream changelog, adapts to breaking changes, regenerates schema, optionally deploys to a dev cluster. | | **/cozystack:external-app-create** | Scaffold a new Cozystack external app package with dependency integration (managed CNPG Postgres, external secret references). | | **/cozystack:dev-ui-bootstrap** | Bootstrap a UI dev sandbox — Playwright + Vite dev server for `cozystack-ui` pointed at a chosen kubeconfig. Creates a feature worktree, installs `@playwright/test` and Chromium, writes `playwright.config.ts`, adds `dev:e2e` / `test:e2e` scripts, and brings up `kubectl proxy` + Vite on a free port. Use when fixing, debugging, or writing tests against the Cozystack console. | +| **/cozystack:comment-audit** | Audit a change's code comments for design-doc prose — product rationale, before/after narrative, incident retelling, editorializing — and report which comments to cut, which earn their place, and which arguments are restated at several sites. Reports only; modifies nothing. Repository-agnostic: works on any codebase, not just Cozystack ones. | +| **/cozystack:comment-trim** | Apply a comment audit — delete the prose, keep every comment that documents a real mechanism, replace sync-with-the-design-doc comments with a pointer, then prove with the bundled `comments-only.py` that no code moved. Leaves the edits in the working tree; does not commit or push. Repository-agnostic. | Chains the wizard builds: @@ -71,7 +73,7 @@ Operators can opt out with `--no-extractedprism` and supply their own `--api-hos ```text plugins/ - cozystack/ # platform bundle (11 skills) + cozystack/ # platform bundle (13 skills) .claude-plugin/plugin.json skills/ wizard/ # entry point: interview + chain dispatcher @@ -85,6 +87,8 @@ plugins/ package-bump/ # bump a monorepo package external-app-create/ # scaffold a new external-apps package dev-ui-bootstrap/ # bootstrap Playwright + Vite sandbox for cozystack-ui + comment-audit/ # report design-doc prose in code comments + comment-trim/ # apply the cuts + prove comments-only linstor/ # storage bundle (1 skill) .claude-plugin/plugin.json skills/ diff --git a/plugins/cozystack/.claude-plugin/plugin.json b/plugins/cozystack/.claude-plugin/plugin.json index ea8c0f1..b1e6a24 100644 --- a/plugins/cozystack/.claude-plugin/plugin.json +++ b/plugins/cozystack/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "cozystack", - "version": "1.15.1", - "description": "Cozystack platform skills bundle. Start with cozystack:wizard — it begins with a free-form 'tell me about your setup and goal' question, parses hints, then asks Talos / Ubuntu / Existing, builds a chain, and dispatches downstream skills via a cluster config directory the operator picks (every artifact lives there: inventory.yml, kubeconfig, .state.yaml, cozystack-platform-package.yaml — operator manages git on their own; optional sops opt-in encrypts secret files in-tree). Skills, invoked as cozystack:: wizard (orchestrator + 3-route dispatcher + Phase 4.5 active research + auto-dispatches debug on any failed_at), talos-bootstrap (Talos node prep via talm — Talos-1.12-aware maintenance probe, NAT-provider cert-SAN guardrail before first talm apply, multidoc machine-config with per-node VIP-link IPv4 stubs, etcd bootstrap, kubeconfig fetch, cozystack-tuned shape verification with Phase 11.5 auto-upgrade), talos-reset (cloud-provider terminate+relaunch helper for OCI/AWS/GCP/Hetzner when nodes are unrecoverable from inside; preserves block volumes + secondary VNICs + NSG memberships), ubuntu-bootstrap (wraps cozystack/ansible-cozystack — OS prep + k3s install in one go), cluster-install (Cozystack on a ready cluster — node-readiness, ZFS pool provisioning via privileged DaemonSet on Talos with hostNetwork, extractedprism for kube-apiserver HA, OCI-tag-normalized cozy-installer chart, Platform Package, inline tenants/root ingress patch + LINSTOR pool registration during watch loop with combined HRs-Ready + pools-registered gate, Phase 8.6 default StorageClasses for v1.3.x, Phase 9.1 end-to-end reachability probe), debug (investigate a stuck or broken install — gathers symptoms, classifies operator error / config drift / upstream bug / not-yet-supported, applies fixes or workarounds, drafts upstream issues on approval; never opens PRs or files silently), cluster-upgrade (v1.x patch/minor upgrade with release-notes analysis), package-deploy (dev-loop deploy of a single package with ExternalArtifact support), package-bump (single-package version bump with changelog adaptation), external-app-create (scaffold a new external-apps package), dev-ui-bootstrap (bootstrap a UI dev sandbox — Playwright + Vite dev server for cozystack-ui pointed at a chosen kubeconfig; worktree, @playwright/test, Chromium, playwright.config.ts, dev:e2e / test:e2e scripts, kubectl proxy + Vite on a free port). All skills match the operator's natural language detected from conversation context — code identifiers, commands, file paths, and GitHub-public text stay canonical. All skills follow the same gate-and-confirm discipline: read-only lookups run freely; any mutation needs explicit per-step approval.", + "version": "1.16.0", + "description": "Cozystack platform skills bundle. Start with cozystack:wizard — it begins with a free-form 'tell me about your setup and goal' question, parses hints, then asks Talos / Ubuntu / Existing, builds a chain, and dispatches downstream skills via a cluster config directory the operator picks (every artifact lives there: inventory.yml, kubeconfig, .state.yaml, cozystack-platform-package.yaml — operator manages git on their own; optional sops opt-in encrypts secret files in-tree). Skills, invoked as cozystack:: wizard (orchestrator + 3-route dispatcher + Phase 4.5 active research + auto-dispatches debug on any failed_at), talos-bootstrap (Talos node prep via talm — Talos-1.12-aware maintenance probe, NAT-provider cert-SAN guardrail before first talm apply, multidoc machine-config with per-node VIP-link IPv4 stubs, etcd bootstrap, kubeconfig fetch, cozystack-tuned shape verification with Phase 11.5 auto-upgrade), talos-reset (cloud-provider terminate+relaunch helper for OCI/AWS/GCP/Hetzner when nodes are unrecoverable from inside; preserves block volumes + secondary VNICs + NSG memberships), ubuntu-bootstrap (wraps cozystack/ansible-cozystack — OS prep + k3s install in one go), cluster-install (Cozystack on a ready cluster — node-readiness, ZFS pool provisioning via privileged DaemonSet on Talos with hostNetwork, extractedprism for kube-apiserver HA, OCI-tag-normalized cozy-installer chart, Platform Package, inline tenants/root ingress patch + LINSTOR pool registration during watch loop with combined HRs-Ready + pools-registered gate, Phase 8.6 default StorageClasses for v1.3.x, Phase 9.1 end-to-end reachability probe), debug (investigate a stuck or broken install — gathers symptoms, classifies operator error / config drift / upstream bug / not-yet-supported, applies fixes or workarounds, drafts upstream issues on approval; never opens PRs or files silently), cluster-upgrade (v1.x patch/minor upgrade with release-notes analysis), package-deploy (dev-loop deploy of a single package with ExternalArtifact support), package-bump (single-package version bump with changelog adaptation), external-app-create (scaffold a new external-apps package), dev-ui-bootstrap (bootstrap a UI dev sandbox — Playwright + Vite dev server for cozystack-ui pointed at a chosen kubeconfig; worktree, @playwright/test, Chromium, playwright.config.ts, dev:e2e / test:e2e scripts, kubectl proxy + Vite on a free port), comment-audit (audit a diff's code comments for design-doc prose — rationale, product motivation, before/after narrative, incident retelling — and report cuts, keeps and arguments duplicated across sites; reports only, modifies nothing), comment-trim (apply the cuts — delete the prose, keep every comment that documents a real mechanism, then prove mechanically with the bundled comments-only.py that no code moved; leaves the result in the working tree, does not commit or push). comment-audit and comment-trim are repository-agnostic and usable on any codebase. All skills match the operator's natural language detected from conversation context — code identifiers, commands, file paths, and GitHub-public text stay canonical. All skills follow the same gate-and-confirm discipline: read-only lookups run freely; any mutation needs explicit per-step approval.", "author": { "name": "Cozystack", "url": "https://github.com/cozystack" diff --git a/plugins/cozystack/skills/comment-audit/SKILL.md b/plugins/cozystack/skills/comment-audit/SKILL.md new file mode 100644 index 0000000..6c0e436 --- /dev/null +++ b/plugins/cozystack/skills/comment-audit/SKILL.md @@ -0,0 +1,114 @@ +--- +name: comment-audit +description: Audit code comments for design-doc prose — rationale, product motivation, before/after narrative, incident retelling — and report which ones to cut, which to keep, and which arguments are restated at several sites. Use this whenever reviewing a diff or PR that reads comment-heavy, whenever someone says a change has "too many comments", "excessive comments", "comment bloat", "essays in the code", or asks whether comments are justified, and as a matter of course on any PR that adds a large block of commentary to a function or package doc. Reports only; pair with `cozystack:comment-trim` to apply the cuts. Language-agnostic — works on any repository, not only Cozystack ones. +argument-hint: "[PR number | file | diff base] [--all] (default: lines the change added)" +--- + +# cozystack:comment-audit + +Decide which comments in a change carry their weight and which are a design doc pasted into the source, then report both lists with line anchors and quoted text so a human can act without re-reading the diff. + +This skill **does** read the working tree, the diff, and the repository's docs, and **does** produce a written report. It **does not** edit files, stage, commit, push, or comment on a PR. It is read-only end to end — there is nothing to gate, because nothing mutates. Use `cozystack:comment-trim` to apply the cuts. + +Work in reasoning mode. Follow the phases in order. Use the phrasing "`cozystack:comment-audit`" (not "the skill") in messages to the user, and announce phase transitions: `cozystack:comment-audit Phase N — `. + +Match the operator's natural language detected from prior conversation messages — use it in the report's prose, section headings, and justifications. Quoted comment text, file paths, line anchors, and code identifiers stay verbatim in their original form. + +## The rule + +A comment earns its place when it tells the reader something the code cannot: + +- **Non-obvious mechanism** — an aliasing or mutation hazard, an ordering constraint, a pointer receiver that mutates its receiver, a value that must be copied before use. +- **Why not the obvious approach** — a reader would reach for X; X is wrong here, and here's the mechanism that makes it wrong. This is the single most valuable kind. +- **A deliberate omission that reads as a bug** — why this error is swallowed, why this interface skips a validator, why this field is not checked. +- **A non-local constraint** — "only the key set is read", "must match the vocabulary declared in Y", "callers rely on this ordering". +- **Silent failure** — whenever getting it wrong produces no error, just a wrong answer that looks right. Bias hard toward keeping these. + +A comment is design-doc prose when it argues rather than informs: + +- **Product or business rationale** — "this is a product requirement, not an optimization", "users asked for this in feedback". +- **Evolution narrative** — "before this change", "weaker than what shipped previously", "used to accept". The reader does not have the old behavior and should not need it. +- **Incident retelling** — "this is the bug that took the API down for an hour last quarter". Post-mortem material. +- **UX reasoning** — "so the user can act without a support round-trip". +- **Defending against an alternative nobody proposed** — paragraphs justifying a choice that was never contested. +- **Editorializing** — "the single most load-bearing line in this file". +- **Restating the code** — the comment and the line below it say the same thing. + +The test is not length. A twelve-line comment about a mutation hazard is fine; a three-line one about why the product wants this is not. + +## Do not judge by density + +Resist computing comment-to-code ratios and comparing against neighboring files. If the file or its neighbours already carry design-doc prose, the local average is exactly the thing under review — matching it certifies the problem. Judge each comment on its own register. Ratios are at most a way to pick which file to read first, never evidence for a finding. + +## Phase 1 — Scope the audit + +Default to what the change **added**: the diff against the merge-base (`git diff ...HEAD`, or the PR's merge-base as reported by `gh pr view --json baseRefName`). Pre-existing prose is a separate cleanup — a small change should not be made to fix a style it merely inherited, though it should not extend it either. Call out explicitly which side of that line each finding sits on. + +`--all` audits whole files instead of the added lines. Use it only when the operator asks for it; it turns a review-sized report into a refactor proposal. + +Resolve and state back: the base revision, the file list, and whether the run is diff-scoped or `--all`. + +## Phase 2 — Classify every added comment block + +Read each added comment block **in full** — a block skimmed is a block misclassified. Classify each against the rule above. + +Quote the actual text in the report. A reviewer acting on "too wordy" has nothing to act on; a reviewer acting on a quoted sentence and a line anchor does. + +## Phase 3 — Sweep for duplication + +This is the highest-yield phase and it is easy to miss by reading linearly. The same argument frequently appears in a package or module doc, again inline at the call site, again in the design doc, and again in the PR description. + +Grep for distinctive phrases from each substantial comment block across the repository and the diff: + +```bash +grep -rn "<6-10 distinctive words>" --include='*.' . docs/ +``` + +Two copies of an argument means one of them goes. Where a comment exists to stop code and a design doc drifting apart, the fix is a **pointer**, not a shorter paraphrase: + +```text +// see docs/ §"
" +``` + +A paraphrase is a second copy that will drift; a pointer cannot. + +## Phase 4 — Build the keep list + +Every audit must name the comments that should survive. Without it the request reads as "fewer comments" and comes back as the same arguments in shorter sentences. + +Build this list as you go through Phase 2 rather than as an afterthought — a keep list assembled at the end tends to be thin, because by then the reader is in cutting mode. + +## Report format + +```text +## The rule +[one short paragraph, stated once — not repeated per finding] + +## Cuts +**** +- `:` — "". +... + +## Keep +[the comments that earn their place, with a phrase each on why] + +## Duplication +[each argument that appears at N sites, with all N anchors] + +## Scope note +[what is pre-existing and therefore out of scope] +``` + +State the rule **once, at the top**. Per-bullet justification makes every item read as a taste call and invites a line-by-line argument. + +## Handing off + +When the operator wants the cuts applied, hand off to `cozystack:comment-trim` and pass the report — that skill applies nothing it has not classified, so an audit already in context saves it a full re-read. + +## Anti-goals + +- **Never forecast an aggregate line count.** "This should cut 140–160 lines" sets a number the work is then measured against, and it runs high: most blocks keep a sentence rather than vanishing, so a complete pass lands well under the guess and reads as under-delivery when the estimate was simply wrong. Quote the item list. The list is the ask. +- **Don't sweep test comments in with the rest.** Explaining why a fixture is `96` documents the test's purpose and is legitimate. Flag only the same violations — incident retelling, before/after narrative — and say so explicitly, or the author will strip comments that were doing real work. +- **Don't audit generated files.** Comments in `zz_generated*`, deepcopy, mocks, OpenAPI and vendored code come from a generator; findings there are unactionable. +- **Don't treat operator-facing config docs as code comments.** Comments in `values.yaml`, chart templates, `.env` samples and flag help text are documentation for a different audience, and verbosity there is usually correct. +- **Don't relitigate the code.** If the implementation has a real bug, that is a separate finding in a separate report; do not bury it in a comment audit. diff --git a/plugins/cozystack/skills/comment-trim/SKILL.md b/plugins/cozystack/skills/comment-trim/SKILL.md new file mode 100644 index 0000000..48c2987 --- /dev/null +++ b/plugins/cozystack/skills/comment-trim/SKILL.md @@ -0,0 +1,109 @@ +--- +name: comment-trim +description: Apply a comment trim — delete design-doc prose, rationale essays, before/after narrative and duplicated arguments from code comments while preserving every comment that documents a real mechanism, then prove mechanically that only comments changed. Use this when acting on review feedback about excessive or unjustified comments, when asked to "trim the comments", "cut the prose", "clean up the essays in this file", or as the follow-up to `cozystack:comment-audit`. Leaves the result in the working tree for review rather than committing. Language-agnostic — works on any repository, not only Cozystack ones. +argument-hint: "[audit file | PR number | file] (default: act on the audit in context)" +--- + +# cozystack:comment-trim + +Turn a comment audit into edits. Delete what argues, keep what informs, and finish by proving the change moved no code. + +This skill **does** modify files in the working tree, and **does** run the bundled verifier plus the project's own build / lint / tests for the packages it touched. It **does not** stage, commit, push, open PRs, or comment on a PR — the edits are left in the working tree for a human to review. Gate-and-confirm discipline applies: read-only lookups (diff, grep, verifier) run freely; before the first edit, state the file list and the claims being cut and get an explicit go-ahead; if the operator asks for a commit afterwards, that is a separate approval. + +Work in reasoning mode. Use the phrasing "`cozystack:comment-trim`" (not "the skill") in messages to the user, and announce phase transitions: `cozystack:comment-trim Phase N — `. + +Match the operator's natural language detected from prior conversation messages — use it in prompts, gates, and the final report. The comment text actually written into the source, code identifiers, file paths, and any commit message stay in their canonical form (usually English). + +If no audit exists in context, run the `cozystack:comment-audit` rule set first to produce one — apply nothing you have not classified. + +## The failure this skill exists to prevent + +The natural response to "too much comment prose" is to write the same arguments in fewer words. That fails the review a second time, because length was never the complaint — a rationale belongs in the design doc, the commit message or the PR body, and compressing it does not move it out of the source. + +**Work claim by claim, not block by block.** Enumerate the distinct assertions a comment block makes, decide keep or cut on each one by register, and then act: + +- a **cut** claim must be *absent* afterwards, not shorter; +- a **kept** claim keeps saying what it said, though you may tighten its wording. + +Both operations shrink the block, so line count cannot tell you whether the trim happened. The check that can: name a claim you meant to cut and ask whether the file still asserts it, in any number of words. If it does, you compressed when you should have deleted. + +The specific trap is using compression as a substitute for deletion. Shrink a block that is four-fifths rationale by a factor of four and it is still four-fifths rationale — unchanged in exactly the respect the reviewer objected to. + +**Worked contrast.** Original: + +> Retrying here is a product requirement, not an optimization. A transient network blip losing an upload is the worst experience this feature has, and before this change a single 503 anywhere in the batch discarded every part that had already succeeded. + +Compressed — *fails*. Three claims in, three claims out; 70% shorter and still rationale: + +> Retries exist because a transient blip used to discard an entire batch, which is unacceptable for uploads. + +Deleted, with a pointer — *passes*. "It is a product requirement", "the worst experience this feature has" and "a 503 used to discard the batch" are now absent; a mechanism fact and an address remain: + +> Retries resume from the last acknowledged part, so the per-part handler must be +> idempotent — see `docs/design/upload.md` §"Retry semantics". + +Tightening a **keeper** is not this failure and is often an improvement. A nine-line proof that loses its closing restatement and says the same thing in four has cut a claim and compressed the rest — exactly right. The only caution is churn: a reworded line costs a reviewer more attention than an untouched one, so tighten where it helps and leave the rest alone. + +## Phase 1 — Scope + +Touch only what the change under review added. Pre-existing prose in the same file is a separate cleanup — mixing it in makes a feature diff unreviewable and takes on an argument the author never started. Note in the report what you left and why. + +Leave generated files (`zz_generated*`, deepcopy, mocks, OpenAPI), vendored code, and operator-facing configuration docs (`values.yaml`, chart templates, flag help text) alone. + +State the file list and the per-file claim list, then get the operator's go-ahead before the first edit. + +## Phase 2 — Cut + +**Delete outright** — product and business rationale, evolution narrative ("before this change", "used to"), incident retelling, UX reasoning, paragraphs defending a choice nobody contested, editorializing, and any comment that restates the line below it. + +**Replace with a pointer** where a comment existed to keep the code and a design doc in sync: + +```go +// Retries resume from the last acknowledged part — see +// docs/design/upload.md §"Retry semantics". +``` + +A paraphrase is a second copy of the argument and will drift; a pointer cannot. Cite the real section heading so the reference survives the document being reorganized. + +**Keep the claim** — mechanism, aliasing and mutation hazards, ordering constraints, "why not the obvious approach", deliberate omissions that read as bugs, non-local constraints, and anything whose violation fails silently. Verbatim by default; tighten only where the wording is genuinely in the way. When in doubt about a comment in this class, keep it: a surviving mechanism note costs a reviewer three seconds, and deleting one costs the next person an afternoon. + +**Deduplicate.** Where an argument appears in a package doc and again inline, one of them goes. Keep the copy at the site that constrains the code — usually the branch or call site, since that is where someone is standing when they consider changing it — and delete the distant one. + +**Rewrite tense, don't delete, for past-tense invariants.** "Omitting the tenant prefix let one tenant's entries answer another tenant's reads" is incident retelling, but the invariant underneath is real. It becomes "cache keys must carry the tenant prefix: without it entries collide across tenants and a read returns another tenant's value." + +## Phase 3 — Verify + +Two checks, in order. Neither is optional — a trim whose correctness rests on having read the diff carefully is worth much less than one that has been proven. + +1. **Comments only.** The bundled script strips comments from both sides and compares what is left, so a stray edit cannot hide in a large diff: + + ```bash + python3 "${CLAUDE_PLUGIN_ROOT}/skills/comment-trim/scripts/comments-only.py" [] + python3 "${CLAUDE_PLUGIN_ROOT}/skills/comment-trim/scripts/comments-only.py" --files OLD NEW + ``` + + (If `$CLAUDE_PLUGIN_ROOT` is unset, the script lives at `scripts/comments-only.py` next to this SKILL.md.) + + It is string-aware — URLs inside literals, Go raw strings, rune literals and Rust lifetimes do not fool it. Exit 0 means no code moved; 1 names the files where it did; 2 means a file's language was not recognised and needs a human look. Investigate any non-zero result before reporting; the usual cause is a real accidental edit. + +2. **The project's own checks** for the packages touched — build, linter, and the tests for those packages only, never the whole suite. A comment trim cannot break a test, which is exactly why a failure here means something else went wrong and must be chased rather than waved through. + +## Phase 4 — Report and stop + +Leave the edits in the working tree. Do not commit unless asked — batching the whole trim into one commit after a human has looked at it is cheaper than a string of fixups, and the author may want to fold it into an existing commit. + +Report: + +- what was cut, by file, grouped by category rather than listed line by line; +- what was deliberately kept, so the reader can see the mechanism notes survived; +- the verification result, stated plainly ("comments only; build, lint and the touched packages' tests pass"); +- anything from the audit you did **not** apply, with the reason. Silently skipping an item is worse than declining it. + +If you are asked to commit, the message should state the rule and what moved, not re-argue the rationale — the same discipline the edit is enforcing. "Comments should carry the mechanism, not the product rationale; every argument removed here is already in the design doc" is the whole body. + +## Anti-goals + +- **Don't shorten an essay and call it deleted.** The check is whether the file still asserts the claim, not whether the block got smaller. This is the single most likely way this goes wrong. +- **Don't cut the keep list.** If an audit named comments to preserve and they are gone, the trim overshot and a reviewer now has to restore them from history. +- **Don't strip test comments wholesale.** Explaining why a fixture is `96` is the test documenting its own purpose. Only the same violations apply — narrative and incident retelling. +- **Don't improve the code while you are in there.** A rename or an extracted helper turns a provably-comments-only change into one that needs a real review. diff --git a/plugins/cozystack/skills/comment-trim/scripts/comments-only.py b/plugins/cozystack/skills/comment-trim/scripts/comments-only.py new file mode 100755 index 0000000..75ca3c1 --- /dev/null +++ b/plugins/cozystack/skills/comment-trim/scripts/comments-only.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""Verify that a change touches only comments. + +Strips comments from both sides of a diff and compares what is left. If the +comment-free forms match, no code moved; that is the claim a comment trim needs +to be able to make, and eyeballing a large diff cannot establish it. + +Usage: + comments-only.py [] # default head: working tree + comments-only.py --files OLD NEW # two files directly + +Exit 0 when every changed file is comments-only, 1 when code changed, 2 on a +file whose language is not recognised (reported, never silently passed). +""" +import re +import subprocess +import sys + +C_LIKE = {".go", ".c", ".h", ".cc", ".cpp", ".hpp", ".java", ".js", ".jsx", + ".ts", ".tsx", ".rs", ".swift", ".kt", ".scala", ".cs", ".php", + ".proto", ".dart", ".groovy"} +HASH = {".py", ".rb", ".sh", ".bash", ".zsh", ".yaml", ".yml", ".toml", + ".tf", ".pl", ".r", ".jl", ".nix"} +DASH = {".sql", ".lua", ".hs", ".elm", ".ada"} + + +def strip_c_like(src): + """Remove // and /* */ comments, respecting string, char and raw-string state.""" + out, i, n = [], 0, len(src) + while i < n: + ch = src[i] + if ch == "/" and i + 1 < n and src[i + 1] == "/": + while i < n and src[i] != "\n": + i += 1 + elif ch == "/" and i + 1 < n and src[i + 1] == "*": + i += 2 + while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"): + i += 1 + i += 2 + elif ch == '"': + out.append(ch) + i += 1 + while i < n and src[i] != '"': + if src[i] == "\\": + out.append(src[i]) + i += 1 + if i < n: + out.append(src[i]) + i += 1 + out.append('"') + i += 1 + elif ch == "`": # Go raw string: no escapes, spans lines + out.append(ch) + i += 1 + while i < n and src[i] != "`": + out.append(src[i]) + i += 1 + out.append("`") + i += 1 + elif ch == "'": + # Char literal, or a Rust lifetime ('a) which never closes. Only + # enter string state when a closing quote is plausibly near. + close = src.find("'", i + 1, i + 6) + if close == -1: + out.append(ch) + i += 1 + else: + out.append(src[i:close + 1]) + i = close + 1 + else: + out.append(ch) + i += 1 + return "".join(out) + + +def strip_linewise(src, marker): + """Remove marker-to-end-of-line comments outside of quotes.""" + out = [] + for line in src.split("\n"): + res, i, n, quote = [], 0, len(line), None + while i < n: + ch = line[i] + if quote: + res.append(ch) + if ch == "\\" and i + 1 < n: + res.append(line[i + 1]) + i += 2 + continue + if ch == quote: + quote = None + i += 1 + elif ch in "\"'": + quote = ch + res.append(ch) + i += 1 + elif line.startswith(marker, i): + break + else: + res.append(ch) + i += 1 + out.append("".join(res)) + return "\n".join(out) + + +def strip(src, ext): + if ext in C_LIKE: + return strip_c_like(src) + if ext in HASH: + return strip_linewise(src, "#") + if ext in DASH: + return strip_linewise(src, "--") + return None + + +def normalize(src): + """Collapse whitespace and drop blank lines: removing a comment leaves both.""" + lines = (re.sub(r"\s+", " ", ln).strip() for ln in src.split("\n")) + return [ln for ln in lines if ln] + + +def show(path, ext, old, new): + a, b = strip(old, ext), strip(new, ext) + if a is None: + print(f" ? {path} (unknown extension '{ext}' — check by hand)") + return 2 + a, b = normalize(a), normalize(b) + if a == b: + print(f" ok {path}") + return 0 + print(f" !! {path} CODE CHANGED") + import difflib + for ln in list(difflib.unified_diff(a, b, "before", "after", lineterm="", n=1))[:40]: + print(f" {ln}") + return 1 + + +def git(*args): + return subprocess.run(["git", *args], capture_output=True, text=True) + + +def main(): + if sys.argv[1:2] == ["--files"]: + old_p, new_p = sys.argv[2], sys.argv[3] + ext = "." + new_p.rsplit(".", 1)[-1] + with open(old_p) as f: + old = f.read() + with open(new_p) as f: + new = f.read() + sys.exit(show(new_p, ext, old, new)) + + if not sys.argv[1:]: + print(__doc__) + sys.exit(2) + + base = sys.argv[1] + head = sys.argv[2] if len(sys.argv) > 2 else None + + rng = [base, head] if head else [base] + files = [f for f in git("diff", "--name-only", *rng).stdout.split("\n") if f] + if not files: + print("no changed files") + sys.exit(0) + + worst = 0 + for path in files: + ext = "." + path.rsplit(".", 1)[-1] if "." in path else "" + old = git("show", f"{base}:{path}").stdout + new = git("show", f"{head}:{path}").stdout if head else open(path).read() + worst = max(worst, show(path, ext, old, new)) + + print() + print({0: "COMMENTS ONLY — no code moved.", + 1: "CODE CHANGED — see the files marked !! above.", + 2: "INCONCLUSIVE — some files need a manual check."}[worst]) + sys.exit(worst) + + +if __name__ == "__main__": + main()