diff --git a/plugins/ai-slop/.claude-plugin/plugin.json b/plugins/ai-slop/.claude-plugin/plugin.json
index 7c6f435b05..87225f0dcb 100644
--- a/plugins/ai-slop/.claude-plugin/plugin.json
+++ b/plugins/ai-slop/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "ai-slop",
- "version": "0.3.8",
+ "version": "0.3.9",
"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",
diff --git a/plugins/ai-slop/CHANGELOG.md b/plugins/ai-slop/CHANGELOG.md
index d99231567d..170a0e58d6 100644
--- a/plugins/ai-slop/CHANGELOG.md
+++ b/plugins/ai-slop/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## [0.3.9]
+
+- **Three directory-expansion defects survived 0.3.8.** A tracked file
+ whose name held a non-ASCII byte was dropped with no trace, because
+ `git ls-files` C-quotes those paths unless told otherwise; the listing
+ now sets `core.quotePath=false` and stays newline-delimited. A
+ filesystem walk still ran when git was missing or could not confirm a
+ work tree, while the comment claimed that case was gone; the walk
+ remains the fallback when tracked-files-only is not achievable, and
+ that fallback is now reported on stderr (`git is not on PATH`, or
+ `git could not confirm a work tree`). The new dir-target tests now
+ assert the emitted `file=` path, not only the scan count, so rebuilding
+ paths from `git rev-parse --show-toplevel` fails the suite; they also
+ pin subtree restriction and a non-ASCII filename. Drive-root slash
+ preservation from 0.3.8 is unchanged.
+
## [0.3.8]
- **Directory targets silently fell back to an untracked-inclusive filesystem
diff --git a/plugins/ai-slop/skills/audit/scripts/detect.sh b/plugins/ai-slop/skills/audit/scripts/detect.sh
index 4a16cc6e33..a45a83fcdb 100755
--- a/plugins/ai-slop/skills/audit/scripts/detect.sh
+++ b/plugins/ai-slop/skills/audit/scripts/detect.sh
@@ -266,14 +266,23 @@ fi
# prefix from `git rev-parse --show-toplevel` and its filter from `pwd`, so on
# any host where those disagree no candidate survived the filter and the walk
# below silently replaced the tracked-files listing it was meant to back up.
+# `ls-files` C-quotes any path holding a non-ASCII byte unless
+# `core.quotePath=false`, so a tracked `café.md` (or a filename that itself
+# holds an em dash) would arrive as a literal quoted escape, fail the scan
+# loop's existence test, and produce neither a finding nor a declined row.
+#
# Running `ls-files` with `-C
` needs neither: it is already
# restricted to that directory's subtree and answers in paths relative to it,
# so `` is the only anchor and cannot disagree with itself.
#
# The branch is chosen up front from `--is-inside-work-tree`, never from an
-# empty pipeline, so a walk is only ever the answer for a directory that is
-# genuinely outside a checkout. Inside one, a listing that fails says so on
-# stderr rather than degrading into a different set of files.
+# empty pipeline. A silent walk is only the answer when git is present and
+# reports the directory is genuinely outside a checkout. If git is missing,
+# or git cannot confirm a work tree (safe.directory refusal, unreadable
+# .git, nonzero rev-parse), the walk still runs because tracked-files-only
+# is not achievable, and that fallback is reported on stderr. Inside a
+# confirmed checkout, a listing that fails says so on stderr rather than
+# degrading into a different set of files.
#
# Strip one trailing slash, except when that would turn a Windows drive root
# (`C:/`) into a drive-relative path (`C:`). Windows then treats the target as
@@ -293,13 +302,25 @@ expand_dir_target() {
local dir inside listing status
dir="$(normalize_dir_target "$1")"
- inside="$(git -C "$dir" rev-parse --is-inside-work-tree 2>/dev/null || true)"
+ if ! command -v git >/dev/null 2>&1; then
+ echo "detect.sh: git is not on PATH; directory $dir expanded via filesystem walk (tracked-files-only is not achievable)" >&2
+ find "$dir" -name '*.md' -type f 2>/dev/null
+ return 0
+ fi
+
+ inside="$(git -C "$dir" rev-parse --is-inside-work-tree 2>/dev/null)"
+ status=$?
+ if [[ "$status" -ne 0 ]]; then
+ echo "detect.sh: git could not confirm a work tree under $dir (exit $status); expanding via filesystem walk" >&2
+ find "$dir" -name '*.md' -type f 2>/dev/null
+ return 0
+ fi
if [[ "$inside" != "true" ]]; then
find "$dir" -name '*.md' -type f 2>/dev/null
return 0
fi
- listing="$(git -C "$dir" ls-files '*.md')"
+ listing="$(git -C "$dir" -c core.quotePath=false ls-files '*.md')"
status=$?
if [[ "$status" -ne 0 ]]; then
echo "detect.sh: git ls-files failed under $dir (exit $status); that directory expanded to nothing" >&2
diff --git a/plugins/ai-slop/skills/audit/scripts/detect.test.sh b/plugins/ai-slop/skills/audit/scripts/detect.test.sh
index 512bbe2194..2cb8d88ba7 100755
--- a/plugins/ai-slop/skills/audit/scripts/detect.test.sh
+++ b/plugins/ai-slop/skills/audit/scripts/detect.test.sh
@@ -403,9 +403,14 @@ cat >"$GITDIR/docs/untracked.md" <"$GITDIR/sibling.md" <&1)"
-assert_contains "dir target in git repo: tracked file scanned via ls-files" "$out" "tracked.md"
+assert_contains "dir target in git repo: tracked file scanned via ls-files" "$out" "file=$GITDIR/docs/tracked.md"
assert_contains "dir target in git repo: only the tracked file counts" "$out" "1 files scanned"
+assert_not_contains "dir target in git repo: sibling outside the subtree is not scanned" "$out" "sibling.md"
# Path agreement. One directory has several SPELLINGS on Git Bash: git answers
# the Windows form of the same checkout a shell reaches as "/tmp/...". An
@@ -420,15 +425,19 @@ GITSPELL="$(git -C "$GITDIR/docs" rev-parse --show-toplevel)/docs"
PWDSPELL="$(cd "$GITDIR/docs" && pwd)"
out="$(bash "$DETECT" "$GITSPELL" 2>&1)"
+assert_contains "dir target, git spelling: the emitted path keeps that spelling" "$out" "file=$GITSPELL/tracked.md"
assert_contains "dir target, git spelling: only the tracked file counts" "$out" "1 files scanned"
out="$(bash "$DETECT" "$PWDSPELL" 2>&1)"
+assert_contains "dir target, shell spelling: the emitted path keeps that spelling" "$out" "file=$PWDSPELL/tracked.md"
assert_contains "dir target, shell spelling: only the tracked file counts" "$out" "1 files scanned"
# A trailing slash is the same directory and must expand identically; the
# expansion builds paths by concatenation, so an unnormalized target would emit
# a doubled separator and scan nothing.
out="$(bash "$DETECT" "$GITDIR/docs/" 2>&1)"
+assert_contains "dir target with a trailing slash: the emitted path is not doubled" "$out" "file=$GITDIR/docs/tracked.md"
+assert_not_contains "dir target with a trailing slash: no doubled separator" "$out" "file=$GITDIR/docs//tracked.md"
assert_contains "dir target with a trailing slash: only the tracked file counts" "$out" "1 files scanned"
# Drive-root slash preservation is a string contract, not a host contract: the
@@ -456,6 +465,28 @@ EOF
out="$(bash "$DETECT" "$GITDIR/untrackedonly" 2>&1)"
assert_contains "dir target in git repo, no tracked markdown: expands to nothing" "$out" "0 files scanned"
+# git C-quotes non-ASCII path bytes unless core.quotePath=false. A tracked
+# filename holding an em dash would then fail the scan loop's existence test
+# and vanish from the report. The listing must emit the raw filename.
+mkdir -p "$GITDIR/unicode"
+printf 'A tracked em dash %s here.\n' "$EM" >"$GITDIR/unicode/dash${EM}name.md"
+git -C "$GITDIR" add "unicode/dash${EM}name.md"
+out="$(bash "$DETECT" "$GITDIR/unicode" 2>&1)"
+assert_contains "dir target, non-ASCII filename: the raw path is scanned" "$out" "file=$GITDIR/unicode/dash${EM}name.md"
+assert_contains "dir target, non-ASCII filename: the file is not dropped" "$out" "1 files scanned"
+
+# git absent: tracked-files-only is not achievable, so the walk still runs,
+# but the fallback must be reported rather than silent.
+NOGIT_BIN="$TEST_TMPDIR/bin-nogit"
+mkdir -p "$NOGIT_BIN"
+for name in bash find sort awk sed cat printf mkdir uname env dirname basename head tail wc tr; do
+ src="$(command -v "$name" 2>/dev/null)" || continue
+ ln -s "$src" "$NOGIT_BIN/$name"
+done
+out="$(PATH="$NOGIT_BIN" bash "$DETECT" "$GITDIR/docs" 2>&1)"
+assert_contains "dir target, git absent: reports the walk" "$out" "git is not on PATH"
+assert_contains "dir target, git absent: walk scans tracked and untracked markdown" "$out" "2 files scanned"
+
# --- Excerpt truncation at the byte boundary --------------------------------------
# 79 ASCII bytes then an em dash: a byte cut at 80 would keep only the first