diff --git a/plugins/ai-slop/.claude-plugin/plugin.json b/plugins/ai-slop/.claude-plugin/plugin.json
index d546dc1b5..7c6f435b0 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.7",
+ "version": "0.3.8",
"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 cfd78e4d1..d99231567 100644
--- a/plugins/ai-slop/CHANGELOG.md
+++ b/plugins/ai-slop/CHANGELOG.md
@@ -1,5 +1,28 @@
# Changelog
+## [0.3.8]
+
+- **Directory targets silently fell back to an untracked-inclusive filesystem
+ walk when git and the shell spelled the same checkout differently.**
+ `detect.sh` built its expansion prefix from `git rev-parse --show-toplevel`
+ and its filter from `pwd`. On Git Bash those disagree (`D:/repo` vs
+ `/d/repo`), so no prefixed candidate survived the filter, `grep` exited
+ non-zero, and `|| find` ran in place of the tracked-files listing it was
+ meant to back up. The walk returns untracked and ignored markdown, so a
+ directory target audited files the checkout does not track and said
+ nothing about it. Expansion now runs `git ls-files` with `-C
`,
+ which is already restricted to that directory's subtree and answers in
+ paths relative to it. The caller's own spelling of the directory is the
+ only anchor. The branch is chosen up front from `--is-inside-work-tree`
+ rather than from an empty pipeline, so a filesystem walk is only ever
+ the answer for a directory genuinely outside a checkout; inside one, a
+ listing that fails reports on stderr instead of degrading into a
+ different set of files. A Windows drive-root target such as `C:/` keeps
+ its trailing slash: stripping it produced `C:`, which Windows treats as
+ drive-relative (the cwd on that drive), so `git -C` / `find` can scan
+ the wrong tree or nothing. Ordinary directory targets still lose one
+ trailing slash.
+
## [0.3.7]
- **A branch name beginning with a YAML indicator silently dropped every finding it emitted.**
diff --git a/plugins/ai-slop/skills/audit/scripts/detect.sh b/plugins/ai-slop/skills/audit/scripts/detect.sh
index 275ee2cbb..4a16cc6e3 100755
--- a/plugins/ai-slop/skills/audit/scripts/detect.sh
+++ b/plugins/ai-slop/skills/audit/scripts/detect.sh
@@ -259,17 +259,69 @@ fi
# Directory targets expand to the markdown beneath them (tracked files when the
# directory is inside a git checkout, a filesystem walk otherwise). Without
# this a directory fails the scan loop's -f test and is skipped silently.
+#
+# ONE anchor, the caller's own spelling of the directory. A directory has
+# several spellings on Git Bash: git answers `C:/Users/...` for the same
+# checkout a shell reaches as `/tmp/...`. The earlier expansion built its
+# 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.
+# 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.
+#
+# 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
+# "cwd on that drive", so git -C / find can scan the wrong tree or nothing.
+# Unix root `/` is the same class: stripping leaves empty, so the original
+# spelling is kept. `C:\` is unchanged because this strip only removes `/`.
+normalize_dir_target() {
+ local dir="${1%/}"
+ if [[ -z "$dir" || "$dir" == [A-Za-z]: ]]; then
+ printf '%s\n' "$1"
+ return 0
+ fi
+ printf '%s\n' "$dir"
+}
+
+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 [[ "$inside" != "true" ]]; then
+ find "$dir" -name '*.md' -type f 2>/dev/null
+ return 0
+ fi
+
+ listing="$(git -C "$dir" 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
+ return 0
+ fi
+
+ while IFS= read -r rel; do
+ [[ -n "$rel" ]] || continue
+ if [[ "$dir" == */ || "$dir" == *\\ ]]; then
+ printf '%s%s\n' "$dir" "$rel"
+ else
+ printf '%s/%s\n' "$dir" "$rel"
+ fi
+ done <<<"$listing"
+}
+
EXPANDED=()
for t in ${TARGETS[@]+"${TARGETS[@]}"}; do
if [[ -d "$t" ]]; then
while IFS= read -r line; do
[[ -n "$line" ]] && EXPANDED+=("$line")
- done < <(
- git -C "$t" ls-files --full-name '*.md' 2>/dev/null |
- sed "s|^|$(git -C "$t" rev-parse --show-toplevel 2>/dev/null)/|" |
- grep -F "$(cd "$t" && pwd)/" ||
- find "$t" -name '*.md' -type f 2>/dev/null
- )
+ done < <(expand_dir_target "$t")
else
EXPANDED+=("$t")
fi
diff --git a/plugins/ai-slop/skills/audit/scripts/detect.test.sh b/plugins/ai-slop/skills/audit/scripts/detect.test.sh
index e8293652c..512bbe219 100755
--- a/plugins/ai-slop/skills/audit/scripts/detect.test.sh
+++ b/plugins/ai-slop/skills/audit/scripts/detect.test.sh
@@ -59,6 +59,9 @@ assert_not_contains() {
*) pass "$1" ;;
esac
}
+assert_eq() {
+ if [[ "$2" == "$3" ]]; then pass "$1"; else fail "$1" "$3" "$2"; fi
+}
EM=$'\xe2\x80\x94'
CHECKMARK=$'\xe2\x9c\x85'
@@ -404,6 +407,55 @@ out="$(bash "$DETECT" "$GITDIR/docs" 2>&1)"
assert_contains "dir target in git repo: tracked file scanned via ls-files" "$out" "tracked.md"
assert_contains "dir target in git repo: only the tracked file counts" "$out" "1 files scanned"
+# 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
+# expansion that derives its prefix from one source and its filter from another
+# drops every candidate wherever the two disagree, and the walk that backs it up
+# returns untracked markdown in place of the tracked listing. The
+# assertion is that the SPELLING of the target cannot change the answer. On a
+# host where the two forms already agree these two cases restate the one above,
+# which is the point: the invariant holds everywhere, and it is only observable
+# here.
+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: only the tracked file counts" "$out" "1 files scanned"
+
+out="$(bash "$DETECT" "$PWDSPELL" 2>&1)"
+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: only the tracked file counts" "$out" "1 files scanned"
+
+# Drive-root slash preservation is a string contract, not a host contract: the
+# suite does not need Windows. Source the production helper so this case cannot
+# drift from the function expand_dir_target actually calls.
+# shellcheck disable=SC1090
+source <(sed -n '/^normalize_dir_target()/,/^}/p' "$DETECT")
+assert_eq "ordinary trailing slash is stripped" "$(normalize_dir_target "docs/")" "docs"
+assert_eq "nested trailing slash is stripped" "$(normalize_dir_target "C:/tmp/")" "C:/tmp"
+assert_eq "unix root keeps its slash" "$(normalize_dir_target "/")" "/"
+assert_eq "windows drive root keeps its slash" "$(normalize_dir_target "C:/")" "C:/"
+assert_eq "lowercase windows drive root keeps its slash" "$(normalize_dir_target "d:/")" "d:/"
+assert_eq "windows drive-root backslash is unchanged" "$(normalize_dir_target "C:\\")" "C:\\"
+assert_eq "already-drive-relative spelling is left alone" "$(normalize_dir_target "C:")" "C:"
+assert_eq "ordinary path without a slash is unchanged" "$(normalize_dir_target "docs")" "docs"
+
+# The walk is reserved for a directory genuinely outside a checkout. A directory
+# INSIDE one that holds only untracked markdown expands to nothing rather than
+# falling through to a filesystem walk, which is what the documented
+# tracked-files-only contract means.
+mkdir -p "$GITDIR/untrackedonly"
+cat >"$GITDIR/untrackedonly/loose.md" <&1)"
+assert_contains "dir target in git repo, no tracked markdown: expands to nothing" "$out" "0 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