Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Removed
- **`repo-finder`'s Category Preference Order and its `3e` category bonus (10%).** With filtering server-side, every candidate matches the user's filters by construction, so a bonus for matching is a constant added to every row and ranks nothing. Its weight went to signals that discriminate: responsiveness 30 → **35%**, outside-contributor track 20 → **25%**.

### Fixed
- **A `preferences.md` with no `## Filters` block no longer dead-ends the scan.** A file carrying only `## Notes` (or an empty `## Filters` block) compiled to a catch-all and aborted `repo-finder` with `exit 10` and a misleading "`topics: any` with no languages" error the user never wrote — leaving `/contribute` with an empty shortlist. Such a file now means "default filters, plus advisory notes" and falls back to `DEFAULT_PROFILE`. A `## Filters` block with a real typo (`langauges: go`) still fails loud, so the fallback never swallows a malformed filter. New `prefs_has_filters` helper in `scripts/lib/preferences.sh`; covered by `test_build_queries.sh`.
- **`/repo-finder` overrides now inherit saved axes from a non-canonical `## filters` header.** The override path (`--lang`/`--topic`/`--min-stars`) read the saved profile with an exact, case-sensitive `## Filters` matcher while `preferences.sh` accepts `## filters` and spacing variants — so `/repo-finder --topic cli` on a hand-edited lowercase-header profile silently dropped the saved `languages`/`stars` and searched with defaults. Both readers now use the same detector. Covered by the new `test_repo_finder_override_inherit.sh`.

## [0.6.4] — 2026-07-13

## [0.6.3] — 2026-07-12
Expand Down
9 changes: 8 additions & 1 deletion commands/repo-finder.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,14 @@ if [ -n "$LANGS$TOPICS$MIN_STARS" ]; then
# a `## Notes` line that happens to begin `languages:`/`topics:`/`stars:` (copied
# prose) must never be mistaken for a hard filter.
FILTERS=""
[ -f "$SAVED" ] && FILTERS=$(awk '/^## Filters/{f=1;next} /^## /{f=0} f' "$SAVED")
# Detect the block with the SAME matcher scripts/lib/preferences.sh uses
# (case-insensitive, spacing-tolerant). A divergent 'exact ## Filters' reader
# silently fails to inherit saved axes on a '## filters' file and searches with
# defaults instead — the silent-wrong-result this whole feature guards against.
[ -f "$SAVED" ] && FILTERS=$(awk '
/^##[ \t]+/ { f = ($0 ~ /^##[ \t]*[Ff]ilters[ \t]*$/); next }
f
' "$SAVED")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if [ -z "$LANGS" ]; then
LANGS=$(printf '%s\n' "$FILTERS" | sed -n 's/^[[:space:]]*languages:[[:space:]]*//p' | head -1)
fi
Expand Down
11 changes: 11 additions & 0 deletions scripts/lib/preferences.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ _prefs_filter_lines() {
' "$1"
}

# True when the file declares a `## Filters` block with at least one content line
# — i.e. `_prefs_filter_lines` would emit something. A file with only `## Notes`,
# or an empty `## Filters` block, emits nothing: that is "use the default
# filters", not a catch-all to reject, so callers fall back to DEFAULT_PROFILE
# rather than aborting. A malformed line is still content (it emits an `ERR` or a
# bad-key row), so `langauges: go` stays a loud parse error — the fallback can
# never silently swallow a typo'd filter.
prefs_has_filters() {
[ -n "$(_prefs_filter_lines "${1:-$(prefs_path)}")" ]
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Comma-separated list -> one trimmed entry per line. Whitespace inside an entry
# is preserved on purpose; the charset guards below reject it.
_prefs_split() {
Expand Down
7 changes: 6 additions & 1 deletion scripts/repo-finder/build_queries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ nudge_once() {
fi
}

if [ ! -f "$PREFS" ]; then
# A file that exists but declares no ## Filters content (only ## Notes, or an
# empty block) means "default filters, plus advisory notes" — not a catch-all to
# reject. Fall back to DEFAULT_PROFILE. A block WITH content still goes to the
# parser below, which fails loud on a typo, so this never swallows a malformed
# filter (see the notes-only + malformed-abort cases in test_build_queries.sh).
if [ ! -f "$PREFS" ] || ! prefs_has_filters "$PREFS"; then
nudge_once
while IFS= read -r q; do
[ -n "$q" ] && emit "$q"
Expand Down
31 changes: 31 additions & 0 deletions tests/scripts/test_build_queries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,37 @@ bash "$BUILD" --file "$tmpdir/bad.md" --no-nudge >/dev/null 2>&1
rc=$?
[ "$rc" = "10" ] || fail "malformed preferences must exit 10, got $rc (never fall back to defaults)"

# ---------------------------------------------------------------------------
# A file with no ## Filters CONTENT is "default filters + advisory notes", NOT a
# catch-all to reject. It must fall back to DEFAULT_PROFILE, never abort with the
# misleading 'topics: any with no languages' error the user never wrote. The
# malformed-abort test above guards the other direction: a Filters block WITH a
# bad line still exits 10, so the fallback can never swallow a real typo.
# ---------------------------------------------------------------------------

# 12. Notes-only file (no ## Filters block at all) => DEFAULT_PROFILE, exit 0.
write notes_only <<'EOF'
## Notes
Prefer small, focused libraries. I'd rather fix bugs than add features.
EOF
run notes_only > "$tmpdir/notes_only.txt" \
|| fail "notes-only preferences.md aborted the scan instead of using defaults"
diff -u "$GOLDEN" "$tmpdir/notes_only.txt" \
|| fail "notes-only file did not fall back to the default profile"

# 12a. An empty ## Filters block (header, no filter lines) is also 'nothing
# specified' => DEFAULT_PROFILE, not a catch-all abort.
write empty_filters <<'EOF'
## Filters

## Notes
just some guidance
EOF
run empty_filters > "$tmpdir/empty_filters.txt" \
|| fail "empty ## Filters block aborted the scan instead of using defaults"
diff -u "$GOLDEN" "$tmpdir/empty_filters.txt" \
|| fail "empty ## Filters block did not fall back to the default profile"

# 11. Determinism: same profile, two runs, byte-identical output.
[ "$(run cross3)" = "$(run cross3)" ] || fail "build is not deterministic"

Expand Down
60 changes: 60 additions & 0 deletions tests/scripts/test_repo_finder_override_inherit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# The /repo-finder override path (`--lang`/`--topic`/`--min-stars`) inherits the
# axes it does NOT name from the saved profile. It must read the saved
# `## Filters` block with the SAME detector scripts/lib/preferences.sh uses —
# case-insensitive, spacing-tolerant. A divergent 'exact ## Filters' matcher
# silently fails to inherit on a '## filters' (lowercase, hand-edited) file and
# searches with default axes instead: the silent-wrong-result this whole feature
# exists to prevent. Network-free, bash 3.2-clean.
set -euo pipefail

ROOT="${CLAUDE_PLUGIN_ROOT:-$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)}"
export CLAUDE_PLUGIN_ROOT="$ROOT"
CMD="$ROOT/commands/repo-finder.md"
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
HOME="$TMP/home"; export HOME
mkdir -p "$HOME/.superhuman"

fail() { echo "FAIL: $*"; exit 1; }

# A saved profile with a NON-canonical header. /preferences writes '## Filters',
# but preferences.sh accepts '## filters' too, so a hand-edited file can look
# like this — and the override path must read it the same way.
cat > "$HOME/.superhuman/preferences.md" <<'EOF'
## filters
languages: go
topics: backend
stars: 3000
EOF

# The override block is the 2nd ```bash fence. Drive it with a --topic override
# (TOPICS set, LANGS/MIN_STARS empty) so languages + stars MUST be inherited from
# the saved profile. Then print the throwaway file the block compiled.
extract_block2() {
awk '/^```bash$/{n++; if(n==2){inb=1; next}} /^```$/{inb=0} inb' "$CMD"
}
{
printf 'LANGS=""\nTOPICS="cli"\nMIN_STARS=""\n'
extract_block2
printf 'echo "---OVERRIDE-FILE---"; cat "$OVERRIDE"\n'
} > "$TMP/block.sh"

OUT=$(bash "$TMP/block.sh" 2>&1) || fail "override block errored: $OUT"

case "$OUT" in
*"languages: go"*) ;;
*) fail "saved 'languages: go' not inherited from a '## filters' file:
$OUT" ;;
esac
case "$OUT" in
*"stars: 3000"*) ;;
*) fail "saved 'stars: 3000' not inherited (fell back to the 2000 default):
$OUT" ;;
esac
case "$OUT" in
*"topics: cli"*) ;;
*) fail "--topic override not applied:
$OUT" ;;
esac

echo "OK test_repo_finder_override_inherit.sh"
Loading