Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/ai-slop/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "ai-slop",
"version": "0.3.4",
"version": "0.3.6",
"description": "Detects and removes AI-writing tells (slop) in checked-in markdown prose: em dashes, emoji formatting, AI vocabulary, negative parallelisms, chatbot phrases, filler, stacked hedging, citation artifacts, and the rest of a catalog distilled from Wikipedia's Signs of AI writing. Read-only audit by default with a deterministic detector plus a judgment rubric; an explicit fix action rewrites findings behind a semantic-diff guard. Findings conform to the detector-findings convention so the review fanout fix relay can consume them.",
"author": {
"name": "Melodic Software",
Expand Down
18 changes: 18 additions & 0 deletions plugins/ai-slop/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## [0.3.6]

- **`emit-findings.sh` double-escaped a pipe the source already escaped, and left Location
absolute when git and `pwd` spelled the same repo root differently.** Both defects were
identified in #3180 and fixed for `docs-hygiene:audit-noise` in #3202; this copy was not
reached. A naive `gsub(/\|/, "\\|")` turns `a \| b` into `a \\| b`, which GFM reads as a
literal backslash followed by a live delimiter, so the row splits and the fix action
misreads it. `esc()` now walks each backslash run and adds a delimiter escape only when
the run length is even, so `a\\|b` (even) becomes a live-safe `a\\\|b` instead of
restoring the original. Separately,
`git rev-parse --show-toplevel` can answer Git's Windows-drive spelling while the caller is at `/tmp/…`
(Git Bash). This producer failed OPEN: Location stayed absolute and nothing reported it,
because an absolute path is still a well-formed cell. Root resolution now prefers the
caller's own `pwd` (minus git's `--show-prefix`) and keeps git's two spellings as
fallbacks. Shared code was considered and declined: plugins are portable and there is no
existing cross-plugin emit-findings library; the three copies now agree on the same two
helpers instead.

## [0.3.4]

- **The README now points back at the upstream ledger.** `docs/upstream/cursor-pstack.md` names
Expand Down
39 changes: 39 additions & 0 deletions plugins/ai-slop/skills/audit/scripts/detect.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,45 @@ EOF
bash "$EMIT" --from "$SYNTH" --out "$TEST_TMPDIR/findings/synth.md" --branch test-branch >/dev/null 2>&1
assert_contains "emit: pipe escaped in cell" "$(cat "$TEST_TMPDIR/findings/synth.md")" 'zero\|tolerance'

# A naive gsub double-escapes a pipe the SOURCE already escaped (`a \| b` ->
# `a \\| b`), which GFM reads as a literal backslash plus a LIVE delimiter.
# This repo writes literal `\|` in its own tables, so the case is real.
ALREADY="$TEST_TMPDIR/already-esc.txt"
cat >"$ALREADY" <<'EOF'
Finding: rule=ai-slop/audit/rule-em-dash file=doc.md line=1 fired=a\|b excerpt=text
Summary rule=ai-slop/audit/rule-em-dash findings=1 declined=0 disabled=0
EOF
ALREADY_OUT="$TEST_TMPDIR/findings/already-esc.md"
bash "$EMIT" --from "$ALREADY" --out "$ALREADY_OUT" --branch test-branch >/dev/null 2>&1
already_row="$(LC_ALL=C grep -m1 '^| 1 ' "$ALREADY_OUT")"
assert_not_contains "emit: an already-escaped pipe is not double-escaped" "$already_row" '\\\|'
assert_contains "emit: and survives as a single-escaped literal" "$already_row" 'a\|b'
already_delims="$(printf '%s' "$already_row" | sed 's/\\|//g' | awk -F'|' '{print NF - 1}')"
Comment on lines +473 to +483

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test-coverage gap in the new esc() idempotency case.

This case only exercises bs=1 (source already carries a single, correctly-escaped \|). The parity branch that actually decides correctness — an even, non-zero backslash run before a pipe (e.g. source excerpt containing a\\|b — two backslashes then a pipe) — has no test in either copy of this suite.

I hand-traced esc() for bs=2: the walk collects bs=2, sees the trailing |, and since 2 % 2 == 0 bumps it to 3, emitting \\\|. Rendered in GFM that's \\ → literal \, then \| → literal | (and the delimiter stays escaped), which matches the pre-table rendering of the two-backslash-then-pipe source. So the logic is correct as merged — but that's exactly the branch Codex flagged as suspect on an earlier revision, and it's the one branch nothing pins down with a regression test today.

Worth adding a sibling case here (and in emit-findings.test.sh) with a source excerpt containing an even, non-zero backslash run before | (e.g. fired=a\\|b), asserting it comes out as a\\\|b and still parses as the expected column count. Cheap insurance against a future edit to esc() silently breaking the one non-obvious parity branch.

Fix this →

assert_contains "emit: so the row still parses as exactly 7 cells" "delims=$already_delims" "delims=8"

# Repo-root spelling mismatch: git's toplevel and the caller's pwd can name
# the same directory differently (Git Bash). This producer fails OPEN — a
# mismatch used to leave Location absolute. A symlink makes pwd and
# --show-toplevel disagree on Linux too.
SPELL_REAL="$TEST_TMPDIR/spell-real"
SPELL_LINK="$TEST_TMPDIR/spell-link"
mkdir -p "$SPELL_REAL"
(
cd "$SPELL_REAL" || exit 1
git init -q .
git config user.email t@example.com
git config user.name Test
)
ln -s "$SPELL_REAL" "$SPELL_LINK"
printf 'Finding: rule=ai-slop/audit/rule-em-dash file=%s/doc.md line=1 fired=zero excerpt=text\n' "$SPELL_LINK" >"$TEST_TMPDIR/spell.txt"
printf 'Summary rule=ai-slop/audit/rule-em-dash findings=1 declined=0 disabled=0\n' >>"$TEST_TMPDIR/spell.txt"
SPELL_OUT="$TEST_TMPDIR/findings/spell.md"
(cd "$SPELL_LINK" && bash "$EMIT" --from "$TEST_TMPDIR/spell.txt" --out "$SPELL_OUT" --branch test-branch) >/dev/null 2>&1
spell_body="$(cat "$SPELL_OUT")"
assert_contains "emit: a pwd-spelled absolute path is relativized" "$spell_body" "| doc.md:1 |"
assert_not_contains "emit: Location carries no absolute prefix" "$spell_body" "$SPELL_LINK/doc.md"
assert_not_contains "emit: Location carries no git-toplevel prefix either" "$spell_body" "$SPELL_REAL/doc.md"

# An excerpt that itself contains file=/line= tokens (a doc describing this
# format) must not spoof the Location cell — first-occurrence parsing.
SPOOF="$TEST_TMPDIR/spoof.md"
Expand Down
82 changes: 79 additions & 3 deletions plugins/ai-slop/skills/audit/scripts/emit-findings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,36 @@ mkdir -p "$(dirname "$OUT")"
# elsewhere binds the FILE NAME (Windows-safe), never this frontmatter field.
DATE_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

LC_ALL=C awk -v branch="$BRANCH" -v date_utc="$DATE_UTC" '
# Repo root, for relativizing Location when detect.sh handed us an absolute
# path. One directory has several SPELLINGS on Git Bash, and matching the
# wrong one leaves every Location absolute — an absolute path is still a
# well-formed cell, so the fail-open producer never reports it. Measured:
# `git rev-parse --show-toplevel` answers Git Bash's Windows spelling of the same temp repo
# while the caller reached the same directory as `/tmp/t/repo`.
#
# The PRIMARY anchor is derived from the caller's own `pwd` by removing the
# sub-path git reports for it. The git-reported forms stay as fallbacks.
# (This producer FAILs OPEN: a path that matches no spelling is left as-is.
# The claude-config sibling fails closed on the same mismatch.)
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
REPO_ROOT_ALT=""
REPO_ROOT_PWD=""
if [[ -n "$REPO_ROOT" ]]; then
REPO_ROOT_ALT="$(cd "$REPO_ROOT" 2>/dev/null && pwd)" || REPO_ROOT_ALT=""
[[ "$REPO_ROOT_ALT" == "$REPO_ROOT" ]] && REPO_ROOT_ALT=""
git_prefix="$(git rev-parse --show-prefix 2>/dev/null || true)"
git_prefix="${git_prefix%/}"
cwd_now="$(pwd)"
if [[ -z "$git_prefix" ]]; then
REPO_ROOT_PWD="$cwd_now"
elif [[ "$cwd_now" == */"$git_prefix" ]]; then
REPO_ROOT_PWD="${cwd_now%/"$git_prefix"}"
fi
[[ "$REPO_ROOT_PWD" == "$REPO_ROOT" || "$REPO_ROOT_PWD" == "$REPO_ROOT_ALT" ]] && REPO_ROOT_PWD=""
fi

LC_ALL=C awk -v branch="$BRANCH" -v date_utc="$DATE_UTC" \
-v repo_root="$REPO_ROOT" -v repo_root_alt="$REPO_ROOT_ALT" -v repo_root_pwd="$REPO_ROOT_PWD" '
# Tier/Action mirror of the severity crosswalk (see header comment).
function rule_tier(slug) {
if (slug == "rule-knowledge-cutoff-disclaimer" || slug == "rule-llm-citation-artifacts" ||
Expand All @@ -136,7 +165,54 @@ LC_ALL=C awk -v branch="$BRANCH" -v date_utc="$DATE_UTC" '
return "Guarded rewrite via /ai-slop:audit fix (judgment; see crosswalk row)"
}
# Cell-escaping rule: literal | becomes \| inside Finding/Action cells.
function esc(s) { gsub(/\|/, "\\|", s); return s }
#
# IDEMPOTENT. A naive gsub double-escapes a pipe the SOURCE already escaped:
# `a \| b` becomes `a \\| b`, which GFM reads as a literal backslash followed
# by a LIVE delimiter — the cell splits and the fix action misreads the row.
# This repo writes literal `\|` in its own tables, so the case is real rather
# than theoretical. Escape by the parity of the complete backslash run before
# each pipe: an odd count already escapes the delimiter; an even count
# (including zero, and `\\|`) leaves it live in GFM and needs one more `\`.
function esc(s, out, i, n, c, bs) {
out = ""
n = length(s)
i = 1
while (i <= n) {
c = substr(s, i, 1)
if (c == "\\") {
bs = 0
while (i <= n && substr(s, i, 1) == "\\") { bs++; i++ }
if (i <= n && substr(s, i, 1) == "|") {
if (bs % 2 == 0) bs++
while (bs--) out = out "\\"
out = out "|"
i++
} else {
while (bs--) out = out "\\"
}
} else if (c == "|") {
out = out "\\|"
i++
} else {
out = out c
i++
}
}
return out
}

# Prefer the caller pwd spelling, then git toplevel, then cd-then-pwd.
# Fail OPEN: a path matching no spelling is returned unchanged (absolute
# Location stays well-formed). That was this producer pre-fix mode.
function relativize(p) {
if (repo_root_pwd != "" && index(p, repo_root_pwd "/") == 1)
return substr(p, length(repo_root_pwd) + 2)
if (repo_root != "" && index(p, repo_root "/") == 1)
return substr(p, length(repo_root) + 2)
if (repo_root_alt != "" && index(p, repo_root_alt "/") == 1)
return substr(p, length(repo_root_alt) + 2)
return p
}

/^Finding: / {
# Split the excerpt off FIRST, on the first " excerpt=" occurrence, then
Expand All @@ -157,7 +233,7 @@ LC_ALL=C awk -v branch="$BRANCH" -v date_utc="$DATE_UTC" '
lno = substr(head, ix + 6)
head = substr(head, 1, ix - 1)
ix = index(head, " file=")
file = substr(head, ix + 6)
file = relativize(substr(head, ix + 6))
slug = substr(head, 1, ix - 1)
t = rule_tier(slug)
row = "| " t " | high | " file ":" lno " | ai-slop:audit | " \
Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-config/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "claude-config",
"version": "0.39.3",
"version": "0.39.4",
"description": "Nine configuration-health skills (plus setup) for a repo's Claude Code configuration: audit (settings.json / .mcp.json / hooks / plugins / permissions drift), audit-automation-gaps (evidence-gated verdicts on automation gaps), audit-permission-grants (allow-rule / allowed-tools grants for auto-mode durability and portability), audit-permission-state (the permission rules actually in effect — every settings scope merged with per-rule provenance, what auto mode drops on entry, config written where nothing reads it, and which managed intents are enforced versus loosenable), draft-auto-mode-rules (interview and draft a paste-ready autoMode classifier block; prints only, never writes), audit-instructions (locally-owned instruction surfaces vs current model capability — proposes removals/rewrites of instructions the model no longer needs, and detects cross-surface instruction conflicts), audit-prompting-postures (the additive lane — posture guidance the prompting guide says a component's purpose needs but the component does not carry), audit-pass (one coordinated, ordered, resumable pass over a named target — three-scope inventory, run-time-derived exclusion set, stable finding identity, suppression memory, resume, one human gate — delegating every check to the plugin that owns it), and unhobble (the empirical bare-baseline experiment: reversibly strip a repo's standing instructions, log real stumbles against the current model, re-add only what evidence earns).",
"author": {
"name": "Melodic Software",
Expand Down
20 changes: 20 additions & 0 deletions plugins/claude-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
All notable changes to the `claude-config` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.39.4]

### Fixed

- **`audit-instructions` `emit-findings.sh` double-escaped a pipe the source already escaped,
and declined every in-repo finding when git and `pwd` spelled the same repo root
differently.** Both defects were identified in #3180 and fixed for `docs-hygiene:audit-noise`
in #3202; this copy was not reached. A naive `gsub(/\|/, "\\|")` turns `a \| b` into
`a \\| b`, which GFM reads as a literal backslash followed by a live delimiter, so the row
splits and the fix action misreads it. `esc()` now walks each backslash run and adds a
delimiter escape only when the run length is even, so `a\\|b` stays a single cell.
Separately, `git rev-parse --show-toplevel` can answer Git's Windows-drive spelling while
the caller is at `/tmp/…` (Git Bash). This producer failed CLOSED: a path it could not
prove was under the root was counted as `outside-repo-root` and silently missed the
relay. Root resolution now prefers the caller's own `pwd` (minus git's `--show-prefix`)
and keeps git's two spellings as fallbacks, then applies the same fail-closed fence.
Shared code was considered and declined: plugins are portable and there is no existing
cross-plugin emit-findings library; the three copies now agree on the same two helpers
instead.

## [0.39.3]

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,42 @@ DATE_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Repo root, for relativizing Location. The fix action fences each remediation to
# its finding's Location, and an absolute path is not portable to the checkout
# that applies the fix.
#
# One directory has several SPELLINGS on Git Bash, and matching the wrong one
# silently declines a real in-repo finding — this producer FAILS CLOSED, so a
# path it cannot prove is under the root never reaches the relay. Measured:
# `git rev-parse --show-toplevel` answers Git Bash's Windows spelling of the same temp repo
# while the caller reached the same directory as `/tmp/t/repo`.
#
# The PRIMARY anchor is derived from the caller's own `pwd` by removing the
# sub-path git reports for it. The git-reported forms stay as fallbacks.
# (The ai-slop sibling fails OPEN on the same mismatch — Location stays
# absolute and nothing reports it.)
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
REPO_ROOT_ALT=""
REPO_ROOT_PWD=""
if [[ -n "$REPO_ROOT" ]]; then
REPO_ROOT_ALT="$(cd "$REPO_ROOT" 2>/dev/null && pwd)" || REPO_ROOT_ALT=""
[[ "$REPO_ROOT_ALT" == "$REPO_ROOT" ]] && REPO_ROOT_ALT=""
git_prefix="$(git rev-parse --show-prefix 2>/dev/null || true)"
git_prefix="${git_prefix%/}"
cwd_now="$(pwd)"
if [[ -z "$git_prefix" ]]; then
REPO_ROOT_PWD="$cwd_now"
elif [[ "$cwd_now" == */"$git_prefix" ]]; then
REPO_ROOT_PWD="${cwd_now%/"$git_prefix"}"
fi
[[ "$REPO_ROOT_PWD" == "$REPO_ROOT" || "$REPO_ROOT_PWD" == "$REPO_ROOT_ALT" ]] && REPO_ROOT_PWD=""
fi

if [[ -n "$CARVEOUT" && ! "$CARVEOUT" =~ ^[0-9]+$ ]]; then
echo "emit-findings.sh: --declined-carveout takes a non-negative integer" >&2
exit 2
fi

LC_ALL=C awk \
-v branch="$BRANCH" -v date_utc="$DATE_UTC" -v repo_root="$REPO_ROOT" -v carveout="$CARVEOUT" '
-v branch="$BRANCH" -v date_utc="$DATE_UTC" -v repo_root="$REPO_ROOT" \
-v repo_root_alt="$REPO_ROOT_ALT" -v repo_root_pwd="$REPO_ROOT_PWD" -v carveout="$CARVEOUT" '
function rule_id(id) {
if (id == "I28-a") return "claude-config/audit-instructions/rule-coercive-emphasis"
if (id == "I28-b") return "claude-config/audit-instructions/rule-blanket-tool-default"
Expand All @@ -163,7 +190,54 @@ LC_ALL=C awk \
return "Replace the blanket default with the targeted condition it stood in for (\"Use [tool] when it would ...\"). The condition is the payload; do not delete the instruction."
}
# Cell-escaping rule: literal | becomes \| inside Finding/Action cells.
function esc(s) { gsub(/\|/, "\\|", s); return s }
#
# IDEMPOTENT. A naive gsub double-escapes a pipe the SOURCE already escaped:
# `a \| b` becomes `a \\| b`, which GFM reads as a literal backslash followed
# by a LIVE delimiter — the cell splits and the fix action misreads the row.
# This repo writes literal `\|` in its own tables, so the case is real rather
# than theoretical. Escape by the parity of the complete backslash run before
# each pipe: an odd count already escapes the delimiter; an even count
# (including zero, and `\\|`) leaves it live in GFM and needs one more `\`.
function esc(s, out, i, n, c, bs) {
out = ""
n = length(s)
i = 1
while (i <= n) {
c = substr(s, i, 1)
if (c == "\\") {
bs = 0
while (i <= n && substr(s, i, 1) == "\\") { bs++; i++ }
if (i <= n && substr(s, i, 1) == "|") {
if (bs % 2 == 0) bs++
while (bs--) out = out "\\"
out = out "|"
i++
} else {
while (bs--) out = out "\\"
}
} else if (c == "|") {
out = out "\\|"
i++
} else {
out = out c
i++
}
}
return out
}

# Prefer the caller pwd spelling, then git toplevel, then cd-then-pwd.
# Empty return means the path is not under any known spelling of the root
# (fail closed — this producer pre-fix mode on a spelling mismatch).
function relativize_in_repo(p) {
if (repo_root_pwd != "" && index(p, repo_root_pwd "/") == 1)
return substr(p, length(repo_root_pwd) + 2)
if (repo_root != "" && index(p, repo_root "/") == 1)
return substr(p, length(repo_root) + 2)
if (repo_root_alt != "" && index(p, repo_root_alt "/") == 1)
return substr(p, length(repo_root_alt) + 2)
return ""
}

# Emit a YAML scalar, quoting ONLY when the plain form would misparse. Git
# accepts branch names beginning with a YAML indicator: "#foo" reads as a
Expand Down Expand Up @@ -296,8 +370,8 @@ LC_ALL=C awk \
# carrying an absolute path, where the fix pass either edits a file outside
# the working tree or consumes the finding without applying it. Neither is
# acceptable, so such rows are declined here and stay in the human report.
if (repo_root == "" || index(file, repo_root "/") != 1) { declined_outofrepo[id]++; next }
loc = substr(file, length(repo_root) + 2)
loc = relativize_in_repo(file)
if (loc == "") { declined_outofrepo[id]++; next }

excerpt = trim(text)
if (length(excerpt) > 160) excerpt = substr(excerpt, 1, 157) "..."
Expand Down
Loading