From b8fb86c57a50ae1662b2f43e625dde3b390b2580 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 14:56:16 +0000 Subject: [PATCH] fix: four probe filters that dropped C-quoted filenames, and a one-directional parity test Four of the marketplace's six extension-filtered pre-computed probes anchored on `$` against unquoted `git status --porcelain` output. Git C-quotes any path holding a space or a metachar, which puts the closing `"` past the extension and off the anchor, so each showed the model a preview missing files with no signal that anything was missing. Proven by execution on a repository holding seven files per extension whose names carry a space, a single quote, a double quote, a semicolon, a pipe, and a newline: the old anchors matched 4 of 7, the new ones match 7 of 7. - docs-hygiene `compress`, `audit-progressive-disclosure`: adopt `audit-noise`'s `grep -E '\.md"?$'`. - docs-hygiene `audit-derivability`: same anchor fix inside 0.21.26's brace group, which is preserved. Roster addition, not a listed finding; found by re-deriving the roster over every `git status --porcelain` injection and again over every extension-anchored filter. Both derivations return the same six probes and no others. - code-tidying `dissolve-comments`: adopt `audit-comment-residue`'s `-z` NUL parse. The line is a preview capped at 10 whose body already tells the model to re-enumerate at scope time, so the impact is bounded. `audit-comment-residue`'s SKILL.md parity test now compares in both directions. Dropping the rename skip from the SKILL.md awk program makes the preview emit a mangled path naming no file, and the suite still passed 53/53. With the reverse loop that mutation fails 1/54 and reverting it passes 54/54. No existing assertion weakened. `claude-ops:observability`'s repo-slug probe rendered `unknown` both on a failed `git rev-parse` and in a repository whose toplevel is literally named `unknown`. The failure case now renders `(git toplevel unavailable)`. Six probe labels no longer assert a bare `empty = none`. The brace group's closing `:` makes the outer `||` unreachable, so a failure inside the group renders empty too: a filter binary off PATH, or a second `git` invocation that fails after the guard's copy succeeded. Not a regression, but the label was making a claim the plumbing cannot back. `playbooks:skill-authoring`'s precompute convention now records the open `pipefail` question: the skills docs name no shell options, the brace-group form is correct under both settings, and a `guard && pipeline || token` shape is correct under only one. Re-ran the state matrix over every touched probe (healthy, at cap, no match, clean tree, outside a repo, each under `set -o pipefail` and `set +o pipefail`): all 70 cells exit 0, at-cap cells render exactly the cap with no failure token, and only the outside-a-repo cells render one. The brace groups from #3469 and #3474 are intact. No grant widened. `audit-progressive-disclosure` keeps `Bash(grep:*)` and its leading token is still `grep`; the other four skills declare no `allowed-tools`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UmrZGdp1dgbZuPCy7tcRJo --- plugins/claude-ops/.claude-plugin/plugin.json | 2 +- plugins/claude-ops/CHANGELOG.md | 14 +++++ .../claude-ops/skills/observability/SKILL.md | 2 +- .../code-tidying/.claude-plugin/plugin.json | 2 +- plugins/code-tidying/CHANGELOG.md | 52 +++++++++++++++++++ .../skills/audit-comment-residue/SKILL.md | 2 +- .../scripts/detect.test.sh | 39 ++++++++++++-- .../skills/dissolve-comments/SKILL.md | 2 +- .../docs-hygiene/.claude-plugin/plugin.json | 2 +- plugins/docs-hygiene/CHANGELOG.md | 52 +++++++++++++++++++ .../skills/audit-derivability/SKILL.md | 2 +- .../docs-hygiene/skills/audit-noise/SKILL.md | 2 +- .../skills/audit-noise/scripts/detect.test.sh | 8 +-- .../audit-progressive-disclosure/SKILL.md | 2 +- plugins/docs-hygiene/skills/compress/SKILL.md | 2 +- plugins/playbooks/.claude-plugin/plugin.json | 2 +- plugins/playbooks/CHANGELOG.md | 21 ++++++++ .../reference/precompute-context.md | 29 ++++++++++- 18 files changed, 215 insertions(+), 22 deletions(-) diff --git a/plugins/claude-ops/.claude-plugin/plugin.json b/plugins/claude-ops/.claude-plugin/plugin.json index 0058e9f3a..36a46fc36 100644 --- a/plugins/claude-ops/.claude-plugin/plugin.json +++ b/plugins/claude-ops/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "claude-ops", - "version": "0.38.17", + "version": "0.38.18", "description": "Claude Code operations toolkit. Twelve skills: audit-skill-visibility (audit whether each installed skill is actually VISIBLE to the model, and diagnose why most of a fleet never gets used \u2014 a skill is invisible when its description is dropped by Claude Code's skill-listing context budget, which drops descriptions least-invoked-first so an unused skill loses the keywords that would let it be matched, from skills genuinely not wanted, from skills the run cannot observe at all; computes whether the listing overflows from documented settings, and withholds every cold verdict the data cannot support rather than reporting absence of data as absence of use), inventory (read-only enumeration of the complete invocable surface \u2014 every built-in CLI command with aliases and hidden/gated status, every bundled skill, and every component of every installed plugin across all marketplaces; reads the shipped binary because upstream publishes no built-in command list, and carries an integrity verdict so a drifted build reports counts as floors rather than silently short totals), audit-install-state (read-only audit of the machine-scope ~/.claude installation directory and ~/.claude.json \u2014 full inventory split into an authored surface and rolled-up bulk trees, product-managed retention vs genuinely unmanaged state, filename-scheme resolution before any process-liveness check, and deliberate/mid-experiment detection; reports, never deletes), audit-performance (read-only slowness-diagnostic capture run at the moment the machine or a session feels slow: CLI version, retention-sweep health including the silent unparsable-settings pause, a timed census walk of the install tree as a sweep-cost proxy, active-session and plugin-fleet counts, a process census, and the fan-out layer, which covers a load-labelled no-op spawn baseline, every hook that will fire bucketed per-tool-call versus per-turn with its invocation shape, the configured statusline, subagent concurrency and spawn-depth ceilings against documented defaults, whether running sessions predate the settings file they are judged by, and orphan attribution by parent liveness rather than age; read against a bundled known-performance-issues reference that also records the causes tested and cleared; separates the four documented suspects of accumulated state, version regression, component bloat, and per-spawn fan-out cost, and routes remediation out; reports, never mutates, and never executes a discovered hook or statusline command), audit-native-overlap (map native Claude Code surfaces \u2014 built-in CLI commands, bundled skills, plugin-backed built-ins, session-provided skills \u2014 against the current repo's plugin skills and agents, so a custom component never silently duplicates what Claude Code itself ships; bare invocation is a read-only overlap report carrying the extraction's integrity floors and a shared-listing-budget exposure section, verdicts are human-gated in a committed store rendered into a generated registry whose every row carries an observable recheck trigger, and only an explicit apply step bakes presence-gated native references into descriptions and Boundary sections), observability (read locally captured telemetry \u2014 OTEL store, collector, hook-event JSONL, ccusage \u2014 with trend reports and store pruning), known-issues (search known Claude product GitHub bugs, check service health, maintain a persistent tracked-issue registry), changelog (ingest Claude Code changelog entries and integrate them into the current repo), plugins (bring a machine's plugin fleet current on demand \u2014 marketplace refresh, effective-scope updates including in-repo project/local installs, new-plugin install per policy, scope-divergence detection and explicit convergence), morning-brief (read-only gh-based operator morning view \u2014 queue-label counts, merge-ready PRs, parked decisions with their RECOMMENDED lines, and loop-lane telemetry freshness), lanes (start/restart/stop/status loop lanes as named background Claude Code sessions seeded from canonical prompt files, with per-lane model/effort, a repo-pull + marketplace-refresh launch step, and a consume-restarts action \u2014 an OS-schedulable reader that relaunches stopped lanes whose telemetry carries a restart_request), and a re-runnable setup action that settles where the known-issues registry lives. Plus a family of eight advisory *-audit hooks (API errors, config changes, instruction loads, permission denials, pre-compaction, skill usage, tool failures, and unsurfaced hook failures \u2014 the last also warns the user via systemMessage, since a hook that fails to launch enforces nothing and Claude Code surfaces the failure to nobody) that emit the shared hook-telemetry envelope, and a reference sink that maps envelopes into the hook-events.jsonl the observability skill reads.", "author": { "name": "Melodic Software", diff --git a/plugins/claude-ops/CHANGELOG.md b/plugins/claude-ops/CHANGELOG.md index 3398ea96b..7b9b8996a 100644 --- a/plugins/claude-ops/CHANGELOG.md +++ b/plugins/claude-ops/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the `claude-ops` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.38.18] + +### Fixed + +- **`observability`: the repo-slug failure token collided with real data.** 0.38.17 made the + `unknown` fallback reachable. It stayed ambiguous: a repository whose toplevel directory is + literally named `unknown` rendered the same string as a `git rev-parse` that failed, so the + reader could not tell a working probe from a broken one. Verified by execution, both states + rendered `unknown` byte for byte. The failure case now renders `(git toplevel unavailable)`, + matching the parenthesized convention the rest of the fleet's probes use, which no directory name + in practice collides with. A repository named `unknown` still renders `unknown`, now + unambiguously. Nothing in the plugin reads the injected value, so no consumer changes. The skill + declares no `allowed-tools`, so no grant changed. LIVE and PRE-EXISTING. Minor. + ## [0.38.17] ### Fixed diff --git a/plugins/claude-ops/skills/observability/SKILL.md b/plugins/claude-ops/skills/observability/SKILL.md index 2a02be101..fc6200f84 100644 --- a/plugins/claude-ops/skills/observability/SKILL.md +++ b/plugins/claude-ops/skills/observability/SKILL.md @@ -13,7 +13,7 @@ metadata: ## Pre-computed context Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` -Repo slug: !`git rev-parse --show-toplevel >/dev/null 2>&1 && git rev-parse --show-toplevel 2>/dev/null | sed 's|.*/||' || echo "unknown"` +Repo slug: !`git rev-parse --show-toplevel >/dev/null 2>&1 && git rev-parse --show-toplevel 2>/dev/null | sed 's|.*/||' || echo "(git toplevel unavailable)"` ccusage availability: !`command -v npx >/dev/null 2>&1 && echo "npx present" || echo "npx MISSING"` Hook event log: !`bash "${CLAUDE_PLUGIN_ROOT}/skills/observability/scripts/probe-observability-state.sh" --hook-events 2>/dev/null || echo "unknown"` OTEL collector :4318: !`bash -c 'source "${CLAUDE_PLUGIN_ROOT}/skills/observability/otel/net-probe.sh" && port_status 4318' 2>/dev/null || echo unknown` diff --git a/plugins/code-tidying/.claude-plugin/plugin.json b/plugins/code-tidying/.claude-plugin/plugin.json index 66403f0d3..910bea933 100644 --- a/plugins/code-tidying/.claude-plugin/plugin.json +++ b/plugins/code-tidying/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "code-tidying", - "version": "0.14.14", + "version": "0.14.15", "description": "Code tidying and comment hygiene: /code-tidying:tidy proactively hunts a rotated, glob-scoped lane for Beck-style tidyings under a research-backed scope budget and ships one tight PR; /code-tidying:batch-simplify sweeps a time window, a branch, or an entire repository through grouped, dependency-ordered simplification waves with a never-drop deferred-items contract; /code-tidying:dissolve-comments enforces self-describing expressive code over a diff or target, widening to the branch diff and then the whole repository when the tree is clean — deletes zero-information comments, dissolves code-expressible ones into names and structure behind a tests gate (safe mode restricts applied edits to removals), and keeps only terse load-bearing comments code cannot express; /code-tidying:audit-comment-residue is a read-only classifier that flags history, plan, conversational, and ticket/PR residue in code comments for author-applied deletion; /code-tidying:audit-dead-code is a read-only whole-repo dead-code hunter running four labelled lanes of unequal confidence (knip for TS/JS, vulture for Python, gopls for Go, and a portable grep lane for shell and other symbol languages), adjudicating every candidate against dynamic-usage evidence into a dead, uncertain, or alive verdict. Project-specific tidy lanes are scaffolded into a tracked .claude/tidy-lanes/ config folder by a re-runnable setup skill.", "author": { "name": "Melodic Software", diff --git a/plugins/code-tidying/CHANGELOG.md b/plugins/code-tidying/CHANGELOG.md index 713cae362..9c79706a5 100644 --- a/plugins/code-tidying/CHANGELOG.md +++ b/plugins/code-tidying/CHANGELOG.md @@ -3,6 +3,58 @@ All notable changes to the `code-tidying` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.14.15] + +### Fixed + +- **`dissolve-comments`'s preview filter dropped every C-quoted filename.** It parsed the porcelain + record with `awk '{print $NF}'`, which splits on whitespace and hands the extension grep a + trailing `"` on any path `git` chose to quote. Proven by execution against a repository holding + seven `.py` files whose names carry, respectively, nothing unusual, a space, a single quote, a + double quote, a semicolon, a pipe, and a newline: the old parse showed 4 of 7, the `-z` NUL parse + it now uses shows 7 of 7. That parse is `audit-comment-residue`'s, adopted verbatim rather than + re-derived, because this site already parses fields and the NUL form is correct for every + filename instead of one class. IMPACT IS BOUNDED and this entry should not overstate it: the line + is a PREVIEW capped at 10 whose own body already instructs the model to re-enumerate the full set + with `git status --porcelain -z` at scope time, so the defect misled the model's first look at + the tree rather than the scope it actually triages. LIVE and PRE-EXISTING: the parse predates + 0.14.12 and 0.14.13, which reworked the shape around it and not the filter. The skill declares no + `allowed-tools`, so no grant changed. One residual, noted rather than fixed: under `-z` a filename + containing a newline reaches the grep as two lines, so it is detected but rendered as its tail. + `head -10` and the re-enumeration instruction already tell the reader the render is partial. +- **The brace group 0.14.13 installed is intact and still correct.** Re-ran the state matrix over + both probes in this plugin (healthy, at cap, no match, clean tree, outside a repo, each under + `set -o pipefail` and `set +o pipefail`, 20 cells): all exit 0, the at-cap cells render exactly + the cap with no failure token, and only the outside-a-repo cells render one. The SIGPIPE + inversion 0.14.13 removed has not come back. + +### Added + +- **`audit-comment-residue`'s SKILL.md parity check is now two-directional.** It compared + `detect.sh`'s audited set against the preview in one direction only: a file `detect.sh` audits + that the preview misses. It could not see the opposite failure, a preview emitting a path + `detect.sh` never audited. Demonstrated by mutation against the shipping tree: dropping the + rename skip from the SKILL.md `awk` program makes the preview print the rename's source record + offset by the status prefix, a path naming no file, and the suite still passed 53 of 53. The + reverse loop now asserts that every path the preview emits is one `detect.sh` audited. Re-running + that exact mutation with the new loop in place fails 1 of 54, naming the phantom path; reverting + the mutation passes 54 of 54. No existing assertion was weakened or removed. PRE-EXISTING blind + spot, test-only change, no shipped behavior touched. + +### Changed + +- **Both probe labels no longer assert `empty = none`.** `audit-comment-residue` and + `dissolve-comments` rendered their filtered preview under a label positively claiming that an + empty render meant no matching files. The probe never established that. The brace group's closing + `:` makes the outer `||` unreachable, so a failure INSIDE the group also renders empty: a filter + binary off PATH, or a second `git` invocation that fails after the guard's copy succeeded, which + models `index.lock` contention. Not a regression, the pre-0.14.13 shape was equally silent in + both states, but the label was making a claim the plumbing cannot back. The labels now read + `empty = none matched or the probe returned nothing`. Fixing the label rather than the plumbing + is deliberate: closing those states needs a temp file or a second capture in a block that must + stay free of `$`, which costs more than the honesty is worth. The parity test extracts the label + by stem, so the wider parenthetical does not disturb it. + ## [0.14.14] ### Fixed diff --git a/plugins/code-tidying/skills/audit-comment-residue/SKILL.md b/plugins/code-tidying/skills/audit-comment-residue/SKILL.md index abf77a2ef..a4a3f39d3 100644 --- a/plugins/code-tidying/skills/audit-comment-residue/SKILL.md +++ b/plugins/code-tidying/skills/audit-comment-residue/SKILL.md @@ -13,7 +13,7 @@ metadata: ## Pre-computed context Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` -Uncommitted code files (empty = none): !`git status --porcelain -z >/dev/null 2>&1 && { git status --porcelain -z 2>/dev/null | awk 'BEGIN { RS = "\0" } skip { skip = 0; next } { if (substr($0, 1, 2) ~ /[RC]/) skip = 1; print substr($0, 4) }' | grep -Ei '\.(cs|ts|tsx|js|jsx|py|sh|ps1|go|rs|java|rb|lua|sql|c|h|cpp|hpp|yaml|yml|toml)$' | head -10; :; } || echo "(git status unavailable)"` +Uncommitted code files (empty = none matched or the probe returned nothing): !`git status --porcelain -z >/dev/null 2>&1 && { git status --porcelain -z 2>/dev/null | awk 'BEGIN { RS = "\0" } skip { skip = 0; next } { if (substr($0, 1, 2) ~ /[RC]/) skip = 1; print substr($0, 4) }' | grep -Ei '\.(cs|ts|tsx|js|jsx|py|sh|ps1|go|rs|java|rb|lua|sql|c|h|cpp|hpp|yaml|yml|toml)$' | head -10; :; } || echo "(git status unavailable)"` Residue findings (sample): !`${CLAUDE_SKILL_DIR}/scripts/detect.sh 2>/dev/null | grep -E '^(Summary total:|Finding shape:)' | head -20 || echo "none"` ## Purpose diff --git a/plugins/code-tidying/skills/audit-comment-residue/scripts/detect.test.sh b/plugins/code-tidying/skills/audit-comment-residue/scripts/detect.test.sh index 23f9f04b7..7ff43b6d5 100755 --- a/plugins/code-tidying/skills/audit-comment-residue/scripts/detect.test.sh +++ b/plugins/code-tidying/skills/audit-comment-residue/scripts/detect.test.sh @@ -245,7 +245,7 @@ assert_not_contains "worktree-column rename is not reported as files=0" "$worktr assert_contains "worktree-column rename resolves to the new path" "$worktree_rename_out" "Summary file: new.py" # --- 10. SKILL.md pre-computed-context parser stays at parity with detect.sh (#3126) ---- -# SKILL.md's `Uncommitted code files (empty = none):` line re-implements the porcelain parse to +# SKILL.md's `Uncommitted code files (…):` line re-implements the porcelain parse to # preview targets to the model. A divergence there is a false negative on the same surface, so the # program is EXTRACTED from SKILL.md and executed rather than being restated here — a copy # would pass while the real line rotted. Fixture names force C-quoting through an embedded @@ -256,8 +256,8 @@ if [[ ! -f "$SKILL_MD" ]]; then fail "SKILL.md located for parity check" "file at $SKILL_MD" "missing" else # Pull the awk program out of: ... | awk '' | grep ... - # Anchor on the label stem, not the whole label: the label carries a parenthetical - # (`(empty = none)`) so the fallback can be read as a failed probe rather than an empty filter. + # Anchor on the label stem, not the whole label: the label carries a parenthetical naming + # what an empty render can mean, so the fallback reads as a failed probe, not an empty filter. skill_awk="$(sed -n "s/^Uncommitted code files[^:]*:.*| awk '\(.*\)' | grep .*$/\1/p" "$SKILL_MD")" if [[ -z "$skill_awk" ]]; then fail "SKILL.md awk program extracted" "non-empty program" "no match — line shape changed" @@ -306,9 +306,17 @@ else assert_not_contains "SKILL.md parser leaves no rename arrow" "$skill_out" ' -> ' assert_not_contains "SKILL.md parser leaves no escaped quote" "$skill_out" '\"' - # Parity with detect.sh over the same tree: every code file detect.sh audits must also - # appear in the preview, or the model is shown a tree the audit does not agree with. + # Parity with detect.sh over the same tree, checked in BOTH directions. Forward: every code + # file detect.sh audits must also appear in the preview, or the model is shown a tree the + # audit does not agree with. Reverse: every path the preview emits must be one detect.sh + # audited, or the model is shown a target that does not exist. A forward-only check cannot + # see an over-reporting preview: drop the awk rename skip and the src record is printed + # offset by the status prefix ("...amed-src.py"), a path naming no file, while every forward + # assertion still passes. REPO13 holds only code files, so the two sets must match exactly; + # a non-code fixture added here would need the preview's extension filter extracted + # alongside the awk program before the reverse direction stays true. detect_out="$(cd "$REPO13" && bash "$DETECT")" + mapfile -t audited_paths < <(printf '%s\n' "$detect_out" | sed -n 's/^Summary file: \(.*\) | T1=.*$/\1/p') parity_ok=1 while IFS= read -r audited; do [[ -z "$audited" ]] && continue @@ -325,6 +333,27 @@ else else fail "SKILL.md preview covers every file detect.sh audits" "full coverage" "see above" fi + + reverse_ok=1 + while IFS= read -r previewed; do + [[ -z "$previewed" ]] && continue + matched=0 + for audited in ${audited_paths[@]+"${audited_paths[@]}"}; do + if [[ "$previewed" == "$audited" ]]; then + matched=1 + break + fi + done + if [[ "$matched" -eq 0 ]]; then + reverse_ok=0 + printf ' preview emitted a path detect.sh did not audit: %s\n' "$previewed" >&2 + fi + done < <(printf '%s\n' "$skill_out") + if [[ "$reverse_ok" -eq 1 ]]; then + pass "SKILL.md preview emits no path detect.sh did not audit" + else + fail "SKILL.md preview emits no path detect.sh did not audit" "no extra paths" "see above" + fi fi fi diff --git a/plugins/code-tidying/skills/dissolve-comments/SKILL.md b/plugins/code-tidying/skills/dissolve-comments/SKILL.md index 7e7295983..3c02877f4 100644 --- a/plugins/code-tidying/skills/dissolve-comments/SKILL.md +++ b/plugins/code-tidying/skills/dissolve-comments/SKILL.md @@ -12,7 +12,7 @@ metadata: ## Pre-computed context Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` -Uncommitted code files (preview, first 10; empty = none): !`git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | awk '{print $NF}' | grep -Ei '\.(cs|ts|tsx|js|jsx|py|sh|ps1|go|rs|java|rb|lua|sql|c|h|cpp|hpp|yaml|yml|toml)$' | head -10; :; } || echo "(git status unavailable)"` +Uncommitted code files (preview, first 10; empty = none matched or the probe returned nothing): !`git status --porcelain -z >/dev/null 2>&1 && { git status --porcelain -z 2>/dev/null | awk 'BEGIN { RS = "\0" } skip { skip = 0; next } { if (substr($0, 1, 2) ~ /[RC]/) skip = 1; print substr($0, 4) }' | grep -Ei '\.(cs|ts|tsx|js|jsx|py|sh|ps1|go|rs|java|rb|lua|sql|c|h|cpp|hpp|yaml|yml|toml)$' | head -10; :; } || echo "(git status unavailable)"` ## Variables diff --git a/plugins/docs-hygiene/.claude-plugin/plugin.json b/plugins/docs-hygiene/.claude-plugin/plugin.json index fe25ce525..697cd1cbe 100644 --- a/plugins/docs-hygiene/.claude-plugin/plugin.json +++ b/plugins/docs-hygiene/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "docs-hygiene", - "version": "0.21.26", + "version": "0.21.27", "description": "Documentation-hygiene toolkit: compress (flavor-trim markdown with a semantic-diff safety net), audit-noise (classify markdown noise), extract-ssot (deduplicate repeated content into a single source of truth), audit-encapsulation (detect citations into skill-private surfaces), rename-references (sweep stale references after renames), audit-derivability (classify whether a whole document earns its existence \u2014 could a fresh agent re-derive it from the code?), audit-progressive-disclosure (grade instruction files against a load-tier model for split opportunities and hub/spoke disclosure defects), write-for-agents (authoring-time doctrine that fires while agent-consumed markdown is being written), and write-for-humans (the same moment for the other reader \u2014 end-user READMEs, RFCs, release notes and guides \u2014 resolving the consuming project's own style guide first).", "author": { "name": "Melodic Software", diff --git a/plugins/docs-hygiene/CHANGELOG.md b/plugins/docs-hygiene/CHANGELOG.md index 4feb861f9..d0ed7e884 100644 --- a/plugins/docs-hygiene/CHANGELOG.md +++ b/plugins/docs-hygiene/CHANGELOG.md @@ -1,5 +1,57 @@ # Changelog — docs-hygiene plugin +## [0.21.27] + +### Fixed + +- **Three `.md` preview filters silently dropped every C-quoted filename.** `git status + --porcelain` wraps any path holding a space or a metachar in double quotes, which puts the + closing `"` past the extension and moves it off a `$` anchor. `compress` and + `audit-progressive-disclosure` anchored with `grep '\.md$'`, `audit-derivability` with + `awk '/\.md$/'`, so each showed the model a preview missing files the skill was about to be + pointed at, with no signal that anything was missing. Proven by execution against a repository + holding seven `.md` files whose names carry, respectively, nothing unusual, a space, a single + quote, a double quote, a semicolon, a pipe, and a newline: the old anchors matched 4 of 7, the + new `\.md"?$` anchors match 7 of 7. The correct form already shipped next door in `audit-noise`, + which keeps its filter unchanged and was the model for the fix. A quoted record still renders + C-quoted in the preview (`?? "with space.md"`); these lines are previews, and every one of them + tells the reader so. LIVE and PRE-EXISTING: the anchors predate 0.21.23, 0.21.24 and 0.21.26, + each of which reworked the shape around them and none of which touched the filters. No grant + changed. `audit-progressive-disclosure` keeps `Bash(grep:*)`, whose leading token is still + `grep`; `compress` and `audit-derivability` declare no `allowed-tools`. +- **`audit-derivability` was a roster addition, not a listed finding.** The report that opened this + lane named two under-reporting `.md` filters in this plugin. Re-deriving the roster over every + `git status --porcelain` injection in the marketplace, and again over every injection carrying an + extension-anchored filter, found a third in `audit-derivability`, whose `awk` regex carries the + same `$` anchor on the same unquoted porcelain. 0.21.26 fixed that probe's SIGPIPE shape and left + its filter as it found it, so the two defects are independent and only one of them is now closed + by that release. Same fix and same proof as the other two. Both derivations return the same six + filtered probes and no others, so the roster is closed: four were defective, `audit-noise` and + `code-tidying`'s `audit-comment-residue` were already correct. +- **The brace groups 0.21.26 and 0.21.24 installed are intact and still correct.** Re-ran the state + matrix over every touched probe (healthy, at cap, no match, clean tree, outside a repo, each + under `set -o pipefail` and `set +o pipefail`, 50 cells for this plugin): all exit 0, the at-cap + cells render exactly the cap with no failure token, and only the outside-a-repo cells render one. + `audit-derivability` was re-verified on the MERGED line, 0.21.26's brace group plus this + release's filter, at 25 dirty `.md` files: 20 paths under both settings, no spurious token. + +### Changed + +- **Four probe labels no longer assert `empty = none`.** `audit-noise`, `compress`, + `audit-progressive-disclosure`, and `audit-derivability` all rendered their filtered preview + under a label positively claiming that an empty render meant no matching files. The probes never + established that. In the brace-group form the closing `:` makes the outer `||` unreachable, so a + failure INSIDE the group also renders empty: a filter binary off PATH, or a second `git` + invocation that fails after the guard's copy succeeded, which models `index.lock` contention. + `audit-derivability`'s capture keeps git's own status outside the pipeline, so it still detects an + unavailable `git`, but its filter stage sits inside the same brace group and fails the same way. + None of this is a regression, the earlier shapes were equally silent, but the label was making a + claim the plumbing cannot back. The labels now read `empty = none matched or the probe returned + nothing`. Fixing the label rather than the plumbing is deliberate: closing those states needs a + temp file or a second capture in a block that must stay free of `$`, which costs more than the + honesty is worth. `audit-noise`'s parity test extracts the label by stem, so the wider + parenthetical does not disturb it; its 199 checks stay green. + ## [0.21.26] ### Fixed diff --git a/plugins/docs-hygiene/skills/audit-derivability/SKILL.md b/plugins/docs-hygiene/skills/audit-derivability/SKILL.md index 2158bebb5..2d71e7fe5 100644 --- a/plugins/docs-hygiene/skills/audit-derivability/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-derivability/SKILL.md @@ -12,7 +12,7 @@ metadata: ## Pre-computed context Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` -Uncommitted .md files (first 20; empty = none): !`s=$(git status --porcelain 2>/dev/null) && { printf '%s\n' "$s" | awk '/\.md$/{p=substr($0,4); sub(/^.* -> /,"",p); print p}' | head -20; :; } || echo "(status unavailable)"` +Uncommitted .md files (first 20; empty = none matched or the probe returned nothing): !`s=$(git status --porcelain 2>/dev/null) && { printf '%s\n' "$s" | awk '/\.md"?$/{p=substr($0,4); sub(/^.* -> /,"",p); print p}' | head -20; :; } || echo "(status unavailable)"` ## Purpose diff --git a/plugins/docs-hygiene/skills/audit-noise/SKILL.md b/plugins/docs-hygiene/skills/audit-noise/SKILL.md index d208fa201..16413cb40 100644 --- a/plugins/docs-hygiene/skills/audit-noise/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-noise/SKILL.md @@ -13,7 +13,7 @@ metadata: ## Pre-computed context Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` -Uncommitted .md files (empty = none): !`git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | grep -E '\.md"?$' | head -10; :; } || echo "(git status unavailable)"` +Uncommitted .md files (empty = none matched or the probe returned nothing): !`git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | grep -E '\.md"?$' | head -10; :; } || echo "(git status unavailable)"` Noise findings (sample): !`${CLAUDE_SKILL_DIR}/scripts/detect.sh 2>/dev/null | grep -E '^(Summary total:|Finding shape:)' | head -20 || echo "none"` ## Purpose diff --git a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh index ae819949e..261ea1394 100755 --- a/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh +++ b/plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh @@ -1331,7 +1331,7 @@ markers_count="$(printf '%s\n' "$markers_out" | grep -c '^Finding shape: negatio assert_contains "both ordered-list delimiters and both checkbox states reach the cue" \ "count=$markers_count" "count=4" -# SKILL.md's `Uncommitted .md files (empty = none):` line previews the same discovery with a grep +# SKILL.md's `Uncommitted .md files (…):` line previews the same discovery with a grep # rather than with detect.sh's parse, so it shares the defect CLASS without sharing the code: git # C-quotes a path it treats specially, and a quoted record ends with the closing quote, not # `.md`. A bare `grep '\.md$'` therefore dropped every spaced, arrowed, backslashed or @@ -1341,9 +1341,9 @@ assert_contains "both ordered-list delimiters and both checkbox states reach the # line rots, which is exactly how the two surfaces drifted apart in the first place. SKILL_MD="$SCRIPT_DIR/../SKILL.md" if [[ -f "$SKILL_MD" ]]; then - # The label carries a parenthetical (`(empty = none)`) and the injection heads its pipeline with - # a status-only probe run, so anchor on the label stem and on the ` | grep … | head` segment - # rather than on the whole line. + # The label carries a parenthetical naming what an empty render can mean, and the injection + # heads its pipeline with a status-only probe run, so anchor on the label stem and on the + # ` | grep … | head` segment rather than on the whole line. skill_grep="$(sed -n 's/^Uncommitted \.md files[^:]*:.*| \(grep [^|]*\) | head.*/\1/p' "$SKILL_MD")" if [[ -n "$skill_grep" ]]; then skill_out="$(cd "$PORC_REPO" && eval "git status --porcelain 2>/dev/null | $skill_grep")" diff --git a/plugins/docs-hygiene/skills/audit-progressive-disclosure/SKILL.md b/plugins/docs-hygiene/skills/audit-progressive-disclosure/SKILL.md index a36b4e6f1..564f563e2 100644 --- a/plugins/docs-hygiene/skills/audit-progressive-disclosure/SKILL.md +++ b/plugins/docs-hygiene/skills/audit-progressive-disclosure/SKILL.md @@ -13,7 +13,7 @@ metadata: ## Pre-computed context Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` -Uncommitted .md files (sample, first 10; empty = none): !`git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | grep '\.md$' | head -10; :; } || echo "(git status unavailable)"` +Uncommitted .md files (sample, first 10; empty = none matched or the probe returned nothing): !`git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | grep -E '\.md"?$' | head -10; :; } || echo "(git status unavailable)"` ## Purpose diff --git a/plugins/docs-hygiene/skills/compress/SKILL.md b/plugins/docs-hygiene/skills/compress/SKILL.md index 7d158bff6..0876f6c4a 100644 --- a/plugins/docs-hygiene/skills/compress/SKILL.md +++ b/plugins/docs-hygiene/skills/compress/SKILL.md @@ -12,7 +12,7 @@ metadata: ## Pre-computed context Current branch: !`git branch --show-current 2>/dev/null || echo "unknown"` -Uncommitted .md files (empty = none): !`git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | grep '\.md$' | head -10; :; } || echo "(git status unavailable)"` +Uncommitted .md files (empty = none matched or the probe returned nothing): !`git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | grep -E '\.md"?$' | head -10; :; } || echo "(git status unavailable)"` ## Purpose diff --git a/plugins/playbooks/.claude-plugin/plugin.json b/plugins/playbooks/.claude-plugin/plugin.json index 4a262458d..43c04740e 100644 --- a/plugins/playbooks/.claude-plugin/plugin.json +++ b/plugins/playbooks/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "playbooks", - "version": "0.9.4", + "version": "0.9.5", "description": "Doctrine and knowledge playbooks as on-demand skills, plus a maintainer-facing update skill. boris — Boris Cherny's Claude Code workflow tips (howborisusesclaudecode.com); skill-authoring — Anthropic's internal skill-authoring playbook; fable-5 — Claude Fable 5's operating doctrine (self-authored, no upstream). The boris and skill-authoring packs vendor a verbatim upstream baseline; /playbooks:update drift-checks and syncs those baselines centrally (maintainers).", "author": { "name": "Melodic Software", diff --git a/plugins/playbooks/CHANGELOG.md b/plugins/playbooks/CHANGELOG.md index b74b92668..b18e6ca39 100644 --- a/plugins/playbooks/CHANGELOG.md +++ b/plugins/playbooks/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to the `playbooks` plugin are recorded here. The `version` i `.claude-plugin/plugin.json` is the delivery vehicle — a consumer receives a change only after that version increases. +## [0.9.5] + +### Added + +- **`skill-authoring`: the precompute convention now records the `pipefail` question.** The file + said nothing about shell options, and the word appeared nowhere in `docs/`, so every author who + reached this convention had to rediscover the same thing: whether Claude Code runs `!` injections + under `set -o pipefail` decides whether a `guard && probe | filter | head -N || echo` shape is + correct or inverted, and the skills docs do not say. They specify the working directory, stderr + merging, timeout, output size and the exit-code semantics of an injected command, and name no + shell options. The new subsection states the question as open, states that the brace-group form + already prescribed above is correct under both settings, and names the two states that make the + `guard && pipeline || token` shape correct under only one: without `pipefail` a failed filter + stage renders empty because the pipeline's status is `head`'s, and with `pipefail` a healthy + at-cap render gains a spurious failure token because `head` closing the pipe SIGPIPEs the stage + upstream. It closes on the brace group's own price, that its final `:` makes the outer `||` + unreachable so a failure inside the group renders empty too, which is why the label must never + assert a bare `empty = none`. The section's recheck trigger now also fires when the docs begin + naming shell options. Documentation only; this is the fix that stops the next author + reintroducing the shape. + ## [0.9.4] ### Changed diff --git a/plugins/playbooks/skills/skill-authoring/reference/precompute-context.md b/plugins/playbooks/skills/skill-authoring/reference/precompute-context.md index 741bf56ed..5ba363896 100644 --- a/plugins/playbooks/skills/skill-authoring/reference/precompute-context.md +++ b/plugins/playbooks/skills/skill-authoring/reference/precompute-context.md @@ -33,8 +33,9 @@ needs an argument Claude derives, or is expensive. ## Conventions we pin -These are Melodic Software conventions, not upstream doctrine. **Recheck trigger:** the skills -docs begin documenting `!` failure/timeout/stderr semantics — revisit these conventions then. +These are Melodic Software conventions, not upstream doctrine. **Recheck trigger:** revisit these +conventions when the skills docs begin documenting `!` failure/timeout/stderr semantics, or the +shell options injections run under. ### Defensive fallback is mandatory @@ -78,6 +79,30 @@ pre-compute block unverifiable to the worktree-isolation guard, and the skill th from an isolated agent. The fleet holds two competing accounts of that guard's trigger, recorded in `session-flow` 0.17.16 and `source-control` 0.51.6; avoiding `$` satisfies both. +### `pipefail` is an open question; the brace group is correct either way + +Whether Claude Code runs `!` injections under `set -o pipefail` is **unsettled**. The skills docs +specify the working directory, stderr merging, timeout, output size, and the exit-code semantics +of an injected command, but name no shell options. Assume either setting, and write a probe that +renders the same under both. + +The brace-group form above does. Its fallback fires on the probe's own exit status and its cap +sits outside the group, so no downstream stage can change what renders. + +A `guard && probe | filter | head -N || echo "(unavailable)"` shape does not. It is correct under +exactly one setting, and which one it needs depends on the state: + +- **Without `pipefail`** the pipeline's status is `head`'s, so the trailing `|| echo` never fires + and a failed filter stage renders empty under a label claiming the opposite. +- **With `pipefail`** `head -N` closes the pipe once it has its N lines, the upstream stage takes + SIGPIPE, and the pipeline exits 141, so a healthy at-cap render gains a spurious + `(unavailable)` line. + +The brace group buys that at a price worth naming. Its final `:` makes the outer `||` unreachable, +so a failure INSIDE the group also renders empty: a filter binary off PATH, or a second `git` +invocation that fails after the guard's copy succeeded. The label carries that weight instead, and +must never assert a bare `empty = none`. + ### Windows / `shell:` awareness `shell:` defaults to `bash`; on Windows without Git Bash the PowerShell tool runs injected