diff --git a/docs/conventions/detector-findings/CHANGELOG.md b/docs/conventions/detector-findings/CHANGELOG.md index fc251ddfb..20392f20a 100644 --- a/docs/conventions/detector-findings/CHANGELOG.md +++ b/docs/conventions/detector-findings/CHANGELOG.md @@ -4,6 +4,18 @@ Notable changes to the detector-findings contract (SemVer). Changing a producer- the coexistence obligations, or an enforceability verdict is a major bump; additive guidance or a new adopter row is a minor bump; docs-only clarification is a patch. +## 2.0.2 — 2026-08-15 + +Docs-only: the self-ignore-guard bullet's consequence sentence was universally +true only where a checkout governs the destination. Where none is detected, the +[topic-docs convention](../topic-docs/README.md) "Runtime guards" now says the +guard does not run, and a producer bound to leave tracked content unmodified +withholds the findings file there too — that destination may be an index-tracked +deletion in the checkout the detection missed, where writing modifies tracked +content instead of creating an untracked path (measured). No producer-owned field +rule, coexistence obligation, or enforceability verdict changes, so this is a +patch. (#2680, #2756 follow-through on #2715) + ## 2.0.1 — 2026-08-15 Patch: no rule changes, one statement corrected to match the rule it was already describing. diff --git a/docs/conventions/detector-findings/README.md b/docs/conventions/detector-findings/README.md index f09a61547..f1b6798f4 100644 --- a/docs/conventions/detector-findings/README.md +++ b/docs/conventions/detector-findings/README.md @@ -55,9 +55,18 @@ What the binding leaves to a producer — consequences, not a second statement o answer to those rungs instead resolves to a directory the consumer never reaches. - **The directory never proves ownership.** What proves a file is this branch's is its own `branch:` frontmatter, never the directory it sits in — the binding's slug rule says why. -- **The self-ignore guard is owed, not re-derived** — including the convention's invalid-root rule, - which stops the guard from healing into a consumer's root `.gitignore`. Skipping it commits - findings that are meant to stay checkout-local. +- **The self-ignore guard is owed, not re-derived** — including the convention's invalid cases, which + stop the guard from healing into a consumer's root `.gitignore` and from writing at a root no + checkout is detected as governing. Skipping it **where a checkout governs the destination** commits + findings that are meant to stay checkout-local. Where none is detected the convention's own rule is + that the guard does not run. **The artifact write is not automatically safe there either**: + recreating a path that is an *index-tracked deletion* in a missed checkout modifies tracked state + rather than creating an untracked one (measured), so "it lands untracked" is not universally true. + But a blanket refusal is the wrong correction — it would refuse the `${CLAUDE_PLUGIN_DATA}` + fallback the convention routes non-interactive runs to, which sits outside every checkout **by + construction** and cannot be a tracked deletion. The rule follows that distinction: write where the + destination is that plugin-data surface, and where it is a resolved root no checkout could be shown + to govern, report the resolved destination and persist nothing. ## Boundary diff --git a/docs/conventions/topic-docs/CHANGELOG.md b/docs/conventions/topic-docs/CHANGELOG.md index 25c89bdbb..648d0cd9e 100644 --- a/docs/conventions/topic-docs/CHANGELOG.md +++ b/docs/conventions/topic-docs/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog — topic-docs convention +## 2.5.0 — 2026-08-15 + +**The self-ignore guard gains a second invalid case: a root no checkout is +detected as governing.** The guard does not run there. Two outcomes bind it — +(A) a memory-tier write is never picked up by a checkout that governs the +destination, and (B) no plugin ever modifies content tracked in any checkout. +The guard is the means to A wherever a governing checkout is found; where none +is detected it buys nothing toward A and can violate B. + +**The rule is blanket by derivation, not by generalizing from one case.** A +`.gitignore` absent from disk is either untracked in some undetected checkout — +where creating it is harmless and even mitigating — or tracked there, where +creating it overwrites committed content and cannot hide the change, since a +tracked file is exempt from its own pattern. Telling those apart requires +querying a checkout, and this branch is defined by having found none, so the +index check that would decide it is the one check that cannot run. The costs are +unequal: guessing "untracked" and being wrong modifies content committed in a +repository the producer cannot see; guessing "tracked" and being wrong forgoes a +mitigation for a harm that is reachable rather than automatic. An undecidable +test with asymmetric outcomes yields *do not write*. + +"Not detected" is stated as a detection claim and never as a claim that none +exists — the branch is entered precisely where detection can be wrong, which is +why the rule is *do not write* rather than *nothing is at risk*. The tracked-file +case was measured on the `core.worktree` topology, where a repository governs a +tree with no `.git` in the destination's path and nothing in the environment to +find; that demonstration is one route into the state, not its definition. + +This closes a self-contradiction rather than carving an exception: the +no-project-root fallback already routes non-interactive runs to +`${CLAUDE_PLUGIN_DATA}` by default — a destination outside every checkout — while +the guard bullet still spoke unconditionally about that same destination. +Non-interactive is the normal condition for forked subagents, dispatched +workers, and headless runs, so every consumer reaching that surface ran +create-when-absent against a root no checkout governs. +() + ## 2.4.4 — 2026-08-15 Docs-only: the Memory, concern-scoped tier row now names `.work/running-retros/` diff --git a/docs/conventions/topic-docs/README.md b/docs/conventions/topic-docs/README.md index 514876a86..943914ee2 100644 --- a/docs/conventions/topic-docs/README.md +++ b/docs/conventions/topic-docs/README.md @@ -389,7 +389,44 @@ cite it rather than redefining it. session, matching the committed-tier guard's scope. A root-equivalent `memory_dir` (`.`, empty, or resolving to the repo root) is **invalid** — stop and surface it; healing there would write `*` into the - consumer's root `.gitignore`, which the next rule forbids. + consumer's root `.gitignore`, which the next rule forbids. A root that + **no checkout is detected as governing** is the second invalid case: + the guard does **not** run there. Two outcomes bind it — (A) a + memory-tier write is never picked up by a checkout that governs the + destination, and (B) no plugin ever modifies content tracked in any + checkout. The guard is the *means* to A wherever a governing checkout + is found; where none is detected it buys nothing toward A, and its + create-when-absent rule can violate B. **"Not detected" is a detection + claim and never a claim that none exists** — the branch is entered + precisely where detection can be wrong. + The rule is blanket because the producer cannot make it conditional: + a `.gitignore` absent from disk is either untracked in some undetected + checkout (where creating it would be harmless, even mitigating) or + tracked there (where creating it overwrites committed content and + cannot hide the change, a tracked file being exempt from its own + pattern). **Telling those apart requires querying a checkout, and this + branch is defined by having found none** — so the index check that + would decide it is exactly the check that cannot run. The costs are + unequal: guessing "untracked" and being wrong modifies content + committed in a repository the producer cannot see, while guessing + "tracked" and being wrong forgoes a mitigation for a harm that is + reachable rather than automatic. An undecidable test with asymmetric + outcomes yields *do not write*. This is not a rare path: the + no-project-root fallback above routes non-interactive runs to + `${CLAUDE_PLUGIN_DATA}` by default, and non-interactive is the normal + condition for forked subagents, dispatched workers, and headless runs. + **The same undecidability binds every other write on that branch**, not + only the guard's: any destination path may be an index-tracked + deletion in the undetected checkout, in which case writing it produces + a tracked modification rather than a new untracked file. A surface + whose artifact must not modify tracked content therefore refuses the + artifact write too, rather than skipping only the guard. **One + destination is exempt and it is the common one**: the + `${CLAUDE_PLUGIN_DATA}` fallback above is outside every checkout by + construction, so it cannot be a tracked deletion and needs no refusal — + which is why this rule is a discrimination between destinations rather + than a blanket stop. What it refuses is a *resolved root* no checkout + could be shown to govern. - No plugin ever edits the consumer's root `.gitignore`. ## Slug and filename spec diff --git a/plugins/architecture/.claude-plugin/plugin.json b/plugins/architecture/.claude-plugin/plugin.json index 0b4b42f83..01fbb415a 100644 --- a/plugins/architecture/.claude-plugin/plugin.json +++ b/plugins/architecture/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "architecture", - "version": "0.5.2", + "version": "0.5.3", "description": "Scans an existing codebase for module-level architecture friction — shallow modules, seam leaks, and locality gaps — using Ousterhout's deep-module lens, presents candidates as a self-contained HTML report, and runs an interview loop on the selected candidate before handing off for planning.", "author": { "name": "Melodic Software", diff --git a/plugins/architecture/CHANGELOG.md b/plugins/architecture/CHANGELOG.md index 142724338..b9103a821 100644 --- a/plugins/architecture/CHANGELOG.md +++ b/plugins/architecture/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to the `architecture` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.5.3] + +### Changed + +- **The binding's Guards section now defers on invalid roots as well as stating the guard.** It said + the self-ignore guard "applies on first write (verify-or-create `.gitignore`)" and named none of + the roots at which the contract says the guard does **not** run — so a reader landed on text + reading as unconditional, including for the no-project-root default where this plugin's memory + writes go. The section now states that such roots exist and points at the convention's "Runtime + guards" for them, **enumerating none**: a second copy of the list is how one rule ends up stated + several ways. + ## [0.5.2] ### Fixed diff --git a/plugins/architecture/reference/topic-docs.md b/plugins/architecture/reference/topic-docs.md index ad7622244..2c840c91d 100644 --- a/plugins/architecture/reference/topic-docs.md +++ b/plugins/architecture/reference/topic-docs.md @@ -3,7 +3,7 @@ How the `improve` skill resolves the destination for its durable per-topic artifact. Implements the topic-docs convention: -. +. The contract owns every general rule — tiers, schema, resolution order, slug spec, runtime guards, no-project-root fallback, non-interactive/forked mode. This document records only this plugin's deltas. @@ -33,4 +33,8 @@ named module or path) → the current branch name. Form and collision rules are ## Guards The memory root's self-ignore guard applies on first write (verify-or-create `.gitignore` with -`*`, announced). Create the topic slice directory when absent. +`*`, announced). The contract also defines **invalid roots at which the guard does not run**; they +are enumerated in its +[Runtime guards](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/topic-docs/README.md#runtime-guards) +section and deliberately not listed here, so this binding cannot drift from them. Create the topic +slice directory when absent. diff --git a/plugins/mutation-testing/.claude-plugin/plugin.json b/plugins/mutation-testing/.claude-plugin/plugin.json index 8dcfa2884..bd883a462 100644 --- a/plugins/mutation-testing/.claude-plugin/plugin.json +++ b/plugins/mutation-testing/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "mutation-testing", - "version": "0.3.2", + "version": "0.3.3", "description": "Measures whether a test suite can actually detect faults, not merely execute code: `/mutation-testing:principles` answers operator, mutant-state, and metric questions from the primary literature; `/mutation-testing:setup` verifies the ecosystem's mutation tool and writes the tracked config; `/mutation-testing:audit` runs diff-scoped mutation analysis and reports surviving mutants, verifying that tracked source was restored and failing the run when it cannot, delegating the productive-versus-arid judgment to a fresh-context reviewer and test authoring to the test lane, and optionally persisting survivors as a findings file the review fix pass consumes.", "author": { "name": "Melodic Software", diff --git a/plugins/mutation-testing/CHANGELOG.md b/plugins/mutation-testing/CHANGELOG.md index 3237f5990..264973940 100644 --- a/plugins/mutation-testing/CHANGELOG.md +++ b/plugins/mutation-testing/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to the `mutation-testing` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.3.3] + +### Changed + +- **The permissive-branch guard rule becomes a pointer.** `--persist-findings` skips the self-ignore + guard where no checkout is detected as governing the destination; that rule now belongs to the + [topic-docs convention](../../docs/conventions/topic-docs/README.md) "Runtime guards", which owns + the guard, so the spoke cites it instead of deriving it locally. Behavior is unchanged — the rule + moved to its owner, where it binds every consumer of that guard rather than this plugin alone. + ## [0.3.2] ### Fixed @@ -113,7 +123,7 @@ All notable changes to the `mutation-testing` plugin are documented here. Format contract rather than restated. Bare invocation is unchanged: it reports and stops. - **Each write this phase makes is proven outside tracked space before that write is made** — the findings file, and the self-ignore guard's own `.gitignore` where a governing checkout was found - (where none was, the guard does not run and the findings file is the only write). Per-write rather + (where none was, neither write happens — see below). Per-write rather than both up front, because on a fresh root the guard's file is what makes the findings file's probe pass. The guard's write is proven *before the guard heals*, by requiring the resolved root to hold no tracked files, because writing `*` into a @@ -130,8 +140,14 @@ All notable changes to the `mutation-testing` plugin are documented here. Format run at all.** There is no repository to keep the write out of, and its create-when-absent rule would otherwise write straight over a `.gitignore` that is absent from disk but tracked in the undiscovered checkout, modifying committed content it could not even hide, since a tracked file is - exempt from its own pattern. What remains on that branch is the findings file alone, which - overwrites nothing. With a governing checkout, `git check-ignore` decides, anchored there and never to the invoking + exempt from its own pattern. **The findings file is withheld on that branch too**, with one + exception: the same undecidability applies to it, since its destination may be an index-tracked + deletion in the undetected checkout, where writing produces a modified tracked file rather than a + new untracked one (measured). The run reports the resolved destination and that nothing was + persisted, rather than writing where it cannot rule that out. The exception is the contract's + `${CLAUDE_PLUGIN_DATA}` fallback for a rootless directory — outside every checkout by + construction, so no tracked deletion can hide there and refusing it would strand the one + destination a headless run on such a directory is meant to use. With a governing checkout, `git check-ignore` decides, anchored there and never to the invoking worktree, where a memory root outside the worktree (a layout the `review:fanout` `fix` action supports explicitly) makes the probe fatal with exit 128 and every write a refusal. "The path is tracked space" and "the probe could not evaluate the path" are reported as the different states diff --git a/plugins/mutation-testing/skills/audit/SKILL.md b/plugins/mutation-testing/skills/audit/SKILL.md index e8ca5d203..19595fa0c 100644 --- a/plugins/mutation-testing/skills/audit/SKILL.md +++ b/plugins/mutation-testing/skills/audit/SKILL.md @@ -317,9 +317,12 @@ are not optional: the destination comes from the contract's **whole** rung order **each** write this phase makes — the findings file, and the self-ignore guard's `.gitignore` where a governing checkout was found — is proven outside tracked space before **that** write is made, against the checkout that governs the destination rather than the invoking worktree, with the guard's own -write proven before the guard heals rather than reported afterwards, and with the guard **not run at -all** where no governing checkout was found, since there its create-when-absent rule could land on a -tracked-but-deleted `.gitignore` with no check having been possible (a memory root inside tracked space leaves `git status` +write proven before the guard heals rather than reported afterwards, and with **nothing written at +all** where a resolved root has no governing checkout — the guard's create-when-absent rule could +land on a tracked-but-deleted `.gitignore` with no check having been possible, and the findings file +on a tracked deletion it would modify rather than create — while the contract's +`${CLAUDE_PLUGIN_DATA}` fallback is written normally, being outside every checkout by construction +(a memory root inside tracked space leaves `git status` identical either way and so cannot detect itself, while a root outside the worktree is a layout the consumer supports and a worktree-anchored probe could only ever refuse); the Phase 4 **verdict class** selects the contract rule and the rule decides `Tier`, never the finding's prose, with diff --git a/plugins/mutation-testing/skills/audit/context/persist-findings.md b/plugins/mutation-testing/skills/audit/context/persist-findings.md index c47cab54a..5d194dfde 100644 --- a/plugins/mutation-testing/skills/audit/context/persist-findings.md +++ b/plugins/mutation-testing/skills/audit/context/persist-findings.md @@ -21,7 +21,9 @@ obligations are the ones a mutation run is most likely to skip, so they are name here: run the **whole** rung order rather than its last rung; take the **non-interactive collapse** for the rungs that confirm or ask, since a headless detector cannot answer; and honor the **self-ignore guard**, including the invalid-root rule that keeps it out of a consumer's root -`.gitignore`. +`.gitignore` — and its second invalid case, named in "Prove the destination is outside tracked +space", where no checkout can be shown to govern a resolved root and this phase writes nothing there +at all. File name: `${TS}-mutation-survivors.md`, with `TS="$(date -u +%Y%m%dT%H%M%SZ)"` — colon-free and Windows-safe, so lexical sort equals chronological sort. @@ -33,11 +35,20 @@ handed to the merge set — the same defect as writing into a file another produ ## Prove the destination is outside tracked space before writing to it -This phase makes **two** writes — the findings file and, when the self-ignore guard heals a root, that -root's `.gitignore` — and the property to prove is that git picks up neither. **Each is proven before -that write is made**, which is the strongest form available and not the same as proving both up -front: on a fresh root the guard's file is exactly what makes the findings file's probe pass, so that -probe cannot precede the guard. The order is what makes the per-write form hold: +This phase makes **at most two** writes — the findings file and, when the self-ignore guard heals a +root, that root's `.gitignore` — and the property to prove is that git picks up neither. **Each is +proven before that write is made**, which is the strongest form available and not the same as proving +both up front: on a fresh root the guard's file is exactly what makes the findings file's probe pass, +so that probe cannot precede the guard. "At most" is load-bearing — where no governing checkout is +found, neither write happens at a resolved root (step 1). + +**The one write that survives that branch is proven by step 1 itself, not by a probe.** The +`${CLAUDE_PLUGIN_DATA}` fallback is written there, and what proves it safe is the agreement of two +independent signals that no checkout governs the path: a destination outside every checkout cannot be +tracked by one, so there is no ignore rule to satisfy and nothing for `check-ignore` to answer. That +is a proof, not an exemption — which is why step 1 needs both signals and why a single-signal version +of it would be fail-open rather than merely weaker. The order below is what makes the per-write form +hold everywhere else: 0. **Make the resolved root a physical path first.** `cd` to its nearest existing ancestor, take `pwd -P`, and re-append the components below it. A lexical walk over a path whose ancestor is a @@ -52,8 +63,9 @@ probe cannot precede the guard. The order is what makes the per-write form hold: run under the **ambient** environment. A toplevel it reports is a governing checkout even when the walk found none. - **No checkout governs the path only when both come back empty**; then, and only then, both writes - proceed. Either signal alone is fail-open in a state the other sees: the walk cannot see a working + **No checkout governs the path only when both come back empty** — and that is the branch on which + this producer writes **nothing**, not the branch on which it writes freely. Either signal alone is + fail-open in a state the other sees: the walk cannot see a working tree designated by `GIT_WORK_TREE`/`GIT_DIR`, where nothing in the path has a `.git` at all yet git reports tracked files there; `rev-parse` cannot tell "no repository" from a missing directory, a dangling `gitdir:`, or a discovery limit, all of which are exit 128. They do not fail on the same @@ -67,26 +79,32 @@ probe cannot precede the guard. The order is what makes the per-write form hold: nothing in the environment to find**. The designation lives in a config file that destination-side discovery never reaches, so both signals come back empty together. - **On the permissive branch, do not run the self-ignore guard at all.** Where no checkout governs - the path there is no repository to keep the write out of, so the guard has no work to do — and the - only circumstance in which its file would have mattered is the one where this discovery was wrong. - Skipping it is what keeps that case harmless. The guard's rule is *create when absent*, and a - `.gitignore` that is absent from disk but **tracked** in an undiscovered checkout — routine after - an `rm` or an interrupted checkout — would be created straight over the consumer's committed - content. A tracked file is exempt from its own pattern, so it could not even hide itself: the - result is a modified tracked file, which is precisely what "never in tracked source" forbids. Step - 3 is what refuses a root holding tracked files, and step 3 never ran here, so the guard would be - writing with its only protection absent. - - What remains on that branch is the findings file alone, written into a directory nothing was found - to govern. If discovery *was* wrong, it shows up in the undiscovered checkout as an untracked - path — visible, attributable, and having overwritten nothing. + **On the permissive branch this producer writes nothing at all — with one exception, below.** Not + the guard's `.gitignore`, and not the findings file. The guard's half is the + [topic-docs convention](../../../../../docs/conventions/topic-docs/README.md) "Runtime guards" + second invalid case, which owns the rule and why; nothing is re-derived here. The findings file + follows for the same undecidability applied to this skill's own contract: the destination may be + an **index-tracked deletion** in the checkout the detection missed, and writing it there produces + a *modified tracked file* rather than a new untracked one — measured. "Read-only with respect to + tracked source" admits no such write, so the branch that cannot rule it out cannot take it. + + Report the resolved destination, say that no checkout could be shown to govern it and the findings + were therefore not persisted, and stop. A refusal a human can act on is the correct end of this + branch; it is not a silent skip. + + **The exception is the contract's `${CLAUDE_PLUGIN_DATA}` fallback**, which the rung order takes + when there is no project root at all. That surface is outside every checkout **by construction**, + so no tracked deletion can hide there and the refusal has nothing to protect — write, and announce + the absolute path as the contract requires. Refusing it would strand the one destination a + headless run on a rootless directory is *supposed* to use. Two cases the signals resolve rather than defer: a **bare** repository with no worktree puts no - `.git` in any ancestor and reports none, so it reaches the permissive branch — correct, since a - repository with no working tree picks nothing up. (A bare *layout* that names a worktree through - `core.worktree` is the topology above, not this one.) A destination **inside a checkout's own - `.git/`** is refused + `.git` in any ancestor and reports none, so it reaches this branch and the run refuses. That + refusal is conservative rather than necessary — a repository with no working tree picks nothing + up — but the signals cannot distinguish it from the `core.worktree` topology, which is the whole + reason the branch refuses; a rule that guessed which one it was facing would be the fail-open this + step exists to prevent. (A bare *layout* that names a worktree through `core.worktree` is that + topology, not this one.) A destination **inside a checkout's own `.git/`** is refused here, by name: `check-ignore` answers exit 1 for it, which step 5 would report as "tracked space", and `.git/` is not that. 2. **Reject a root-equivalent `memory_dir`** — the contract's invalid-root rule, judged against `T` diff --git a/plugins/mutation-testing/skills/audit/evals/evals.json b/plugins/mutation-testing/skills/audit/evals/evals.json index af8da7198..60521f4b8 100644 --- a/plugins/mutation-testing/skills/audit/evals/evals.json +++ b/plugins/mutation-testing/skills/audit/evals/evals.json @@ -52,7 +52,7 @@ { "id": 9, "prompt": "Run mutation testing on my changes and leave the findings somewhere the fix pass can pick them up.", - "expected_output": "Recognizes this as the --persist-findings path and runs Phase 6 after reporting. Reads the detector-findings producer contract at its raw-URL citation before the first write rather than inventing the destination. Resolves the findings location by running the whole rung order, not only the documented default, and runs the self-ignore guard on the RESOLVED memory root -- where a checkout governs that root -- without ever editing the consumer's root .gitignore, and does not run it at all where no checkout governs the destination. Writes exactly one file named `-mutation-survivors.md`, and when that path already exists writes `-2`, then `-3`, taking the smallest free integer >= 2 rather than overwriting. Sets frontmatter `branch:` to `git branch --show-current` verbatim rather than the directory slug. Emits `Location` as a repo-relative `file:line` for the mutated line, `Surface(s)` as `mutation-testing:audit`, and escapes literal pipes in the `Finding` and `Action` cells because mutation fragments carry them. Names the covering test file inside `Action` and states that the remediation site is NOT the row's `Location` -- a mutation finding's fix lands in the test, not at the mutated node, and a consumer that fences each fix to `Location` would otherwise have to guess the target or breach its own fence. Does not restate the findings-file table anywhere.", + "expected_output": "Recognizes this as the --persist-findings path and runs Phase 6 after reporting. Reads the detector-findings producer contract at its raw-URL citation before the first write rather than inventing the destination. Resolves the findings location by running the whole rung order, not only the documented default, and runs the self-ignore guard on the RESOLVED memory root -- where a checkout governs that root -- without ever editing the consumer's root .gitignore, and does not run it at all where no checkout governs the destination -- where a RESOLVED ROOT has no governing checkout it writes nothing at all, neither the guard's `.gitignore` nor the findings file, reporting the resolved destination instead; the contract's `${CLAUDE_PLUGIN_DATA}` fallback is the one destination still written there, being outside every checkout by construction. Otherwise writes exactly one file named `-mutation-survivors.md`, and when that path already exists writes `-2`, then `-3`, taking the smallest free integer >= 2 rather than overwriting. Sets frontmatter `branch:` to `git branch --show-current` verbatim rather than the directory slug. Emits `Location` as a repo-relative `file:line` for the mutated line, `Surface(s)` as `mutation-testing:audit`, and escapes literal pipes in the `Finding` and `Action` cells because mutation fragments carry them. Names the covering test file inside `Action` and states that the remediation site is NOT the row's `Location` -- a mutation finding's fix lands in the test, not at the mutated node, and a consumer that fences each fix to `Location` would otherwise have to guess the target or breach its own fence. Does not restate the findings-file table anywhere.", "files": [] }, { @@ -76,7 +76,7 @@ { "id": 13, "prompt": "Our memory_dir points at a shared directory outside this worktree. Will --persist-findings write there?", - "expected_output": "Yes, once EACH write the phase makes -- the findings file, and the self-ignore guard's `.gitignore` where a governing checkout was found -- is proven outside tracked space before THAT write is made (per-write, not both up front, since on a fresh root the guard's file is what makes the findings file's probe pass). Where NO governing checkout is found, the guard does not run at all and the findings file is the only write: there is no repository to keep it out of, and the guard's create-when-absent rule could otherwise land on a `.gitignore` absent from disk but tracked in an undiscovered checkout, modifying committed content with the tracked-file check never having run. Makes the resolved root physical first via `pwd -P` from its nearest existing ancestor, so a symlinked ancestor cannot hide a checkout. Then resolves the governing checkout from TWO signals that must BOTH come back empty before the permissive branch: a walk of that physical path's ancestors for a `.git` entry using `test -e` (not `-d`, since a worktree's `.git` is a file), AND `git rev-parse --show-toplevel` run under the AMBIENT environment. Neither alone: rev-parse cannot distinguish no-repository from a missing directory, a dangling gitdir, or a discovery limit such as GIT_CEILING_DIRECTORIES -- all exit 128 -- while the walk cannot see a working tree designated by GIT_WORK_TREE or GIT_DIR. Names `core.worktree` as a topology defeating both signals, and says the permissive branch is SHAPED around it rather than merely disclosing it -- skipping the guard is what makes that topology harmless, since the findings file alone overwrites nothing there. With a governing checkout T: rejects a root-equivalent memory_dir judged against T; proves the guard's write BEFORE the guard makes it via `git -C T ls-files -- `, requiring exit 0 AND empty output -- a fatal ls-files prints nothing to stdout and exits 128, so reading output alone lets a failure pass for a clean root; only then runs the guard and `git -C T check-ignore -q -- `, writing only on exit 0. Reports exit 1 ('the destination is tracked space') distinctly from any other exit ('the probe did not evaluate this path'), refusing on both. Anchors the probes to a governing checkout, never the invoking worktree, because check-ignore against an outside path is fatal with exit 128 -- a worktree-anchored probe would refuse every write in a layout the fanout fix action supports explicitly -- and notes that `git -C` alone is not an anchor, since GIT_DIR and GIT_WORK_TREE override it. Not a path-prefix comparison, which would need BOTH paths canonicalized where realpath and readlink's canonicalizing flag are GNU-only.", + "expected_output": "Yes, once EACH write the phase makes -- the findings file, and the self-ignore guard's `.gitignore` where a governing checkout was found -- is proven outside tracked space before THAT write is made (per-write, not both up front, since on a fresh root the guard's file is what makes the findings file's probe pass). Where NO governing checkout is found, the guard does not run at all and the findings file is the only write: there is no repository to keep it out of, and the guard's create-when-absent rule could otherwise land on a `.gitignore` absent from disk but tracked in an undiscovered checkout, modifying committed content with the tracked-file check never having run. Makes the resolved root physical first via `pwd -P` from its nearest existing ancestor, so a symlinked ancestor cannot hide a checkout. Then resolves the governing checkout from TWO signals that must BOTH come back empty before the permissive branch: a walk of that physical path's ancestors for a `.git` entry using `test -e` (not `-d`, since a worktree's `.git` is a file), AND `git rev-parse --show-toplevel` run under the AMBIENT environment. Neither alone: rev-parse cannot distinguish no-repository from a missing directory, a dangling gitdir, or a discovery limit such as GIT_CEILING_DIRECTORIES -- all exit 128 -- while the walk cannot see a working tree designated by GIT_WORK_TREE or GIT_DIR. Names `core.worktree` as a topology defeating both signals, and says that where BOTH signals come back empty the run writes NOTHING at a resolved root -- not the guard's `.gitignore`, not the findings file -- reporting the resolved destination and stopping. Gives the reason the findings file is withheld too: its destination may be an index-tracked DELETION in the checkout the detection missed, where writing modifies a tracked file rather than creating an untracked one, so 'it lands untracked' is not universally true. Names the ONE exception: the contract's `${CLAUDE_PLUGIN_DATA}` fallback for a rootless directory is written normally, since it sits outside every checkout by construction and cannot be a tracked deletion -- the rule discriminates between destinations rather than stopping every write. Treats a bare repository reaching that branch as a conservative refusal indistinguishable from `core.worktree`, never as a case to write through. With a governing checkout T: rejects a root-equivalent memory_dir judged against T; proves the guard's write BEFORE the guard makes it via `git -C T ls-files -- `, requiring exit 0 AND empty output -- a fatal ls-files prints nothing to stdout and exits 128, so reading output alone lets a failure pass for a clean root; only then runs the guard and `git -C T check-ignore -q -- `, writing only on exit 0. Reports exit 1 ('the destination is tracked space') distinctly from any other exit ('the probe did not evaluate this path'), refusing on both. Anchors the probes to a governing checkout, never the invoking worktree, because check-ignore against an outside path is fatal with exit 128 -- a worktree-anchored probe would refuse every write in a layout the fanout fix action supports explicitly -- and notes that `git -C` alone is not an anchor, since GIT_DIR and GIT_WORK_TREE override it. Not a path-prefix comparison, which would need BOTH paths canonicalized where realpath and readlink's canonicalizing flag are GNU-only.", "files": [] }, { diff --git a/plugins/review/.claude-plugin/plugin.json b/plugins/review/.claude-plugin/plugin.json index 2041202ce..44520184f 100644 --- a/plugins/review/.claude-plugin/plugin.json +++ b/plugins/review/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "review", - "version": "0.21.0", + "version": "0.21.1", "description": "Code-review toolkit: six read-only reviewer agents (code, security, architecture, doc drift, build/test/lint, CI-log audit) plus orchestration skills — quality gate, fan-out, and CI lane commands (/review:code-review, /review:security-review) for org reusable workflows.", "author": { "name": "Melodic Software", diff --git a/plugins/review/CHANGELOG.md b/plugins/review/CHANGELOG.md index e5679a5c3..86e201fcb 100644 --- a/plugins/review/CHANGELOG.md +++ b/plugins/review/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to the `review` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.21.1] + +### Changed + +- **The binding's self-ignore-guard bullet now defers on invalid roots as well as on cadence.** It + already restated the guard's create-when-absent behavior and ended "per the contract", but named + none of the roots at which the contract says the guard does **not** run — so a reader arriving + through the detector-findings owner table, which names this binding as the guard's owner, met text + reading as unconditional. The bullet now states that such roots exist and points at the + convention's "Runtime guards" for them, **enumerating none**: a second copy of the list is how a + rule ends up stated several ways, and the omission this repairs was itself an incomplete copy. + ## [0.21.0] ### Added diff --git a/plugins/review/reference/topic-docs.md b/plugins/review/reference/topic-docs.md index d415ee5f3..f75e17e5d 100644 --- a/plugins/review/reference/topic-docs.md +++ b/plugins/review/reference/topic-docs.md @@ -4,7 +4,7 @@ How `/review:quality-gate` and `/review:fanout` resolve where review reports land in a consuming repo. Both skills read this one document; neither bakes its own paths. Implements the topic-docs convention: -. +. The contract owns the tier table, concern-file schema, slug spec, and lifecycle; this document binds this plugin's artifacts to it. @@ -67,5 +67,8 @@ before any write — the convention's no-project-root fallback surface never com - **Self-ignore guard:** the session's first memory-tier write verifies the **resolved memory root** (whatever `memory_dir` names — never a hardcoded `.work`) contains a `.gitignore` with `*`, creating it (announced) when absent — fresh clones heal on first write. Once per session, - per the contract. + per the contract. The contract also defines **invalid roots at which the guard does not run**; + they are enumerated in its + [Runtime guards](https://github.com/melodic-software/claude-code-plugins/blob/main/docs/conventions/topic-docs/README.md#runtime-guards) + section and deliberately not listed here, so this binding cannot drift from them. - No skill in this plugin ever edits the consumer's root `.gitignore`.