@onlooker-community/ecosystem 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.release-please-manifest.json +2 -2
  4. package/CHANGELOG.md +7 -0
  5. package/CLAUDE.md +2 -0
  6. package/docs/plugin-catalog.md +125 -0
  7. package/package.json +3 -3
  8. package/plugins/compass/.claude-plugin/plugin.json +1 -1
  9. package/plugins/compass/CHANGELOG.md +7 -0
  10. package/plugins/compass/README.md +1 -3
  11. package/plugins/compass/config.json +1 -2
  12. package/plugins/compass/docs/design.md +1 -2
  13. package/plugins/compass/scripts/hooks/compass-bash-gate.sh +8 -1
  14. package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +8 -1
  15. package/plugins/compass/scripts/hooks/compass-record-write.sh +5 -0
  16. package/plugins/compass/scripts/hooks/compass-session-start.sh +0 -8
  17. package/plugins/compass/scripts/lib/compass-evaluator.sh +58 -98
  18. package/plugins/compass/scripts/lib/compass-gate.sh +15 -18
  19. package/plugins/compass/scripts/lib/compass-sanitizer.sh +4 -4
  20. package/plugins/compass/scripts/lib/compass-transcript.sh +79 -112
  21. package/plugins/inspector/.claude-plugin/plugin.json +14 -0
  22. package/plugins/inspector/README.md +155 -0
  23. package/plugins/inspector/config.json +25 -0
  24. package/plugins/inspector/docs/design.md +286 -0
  25. package/plugins/inspector/hooks/hooks.json +33 -0
  26. package/plugins/inspector/scripts/hooks/inspector-post-write.sh +124 -0
  27. package/plugins/inspector/scripts/lib/inspector-config.sh +108 -0
  28. package/plugins/inspector/scripts/lib/inspector-events.sh +82 -0
  29. package/plugins/inspector/scripts/lib/inspector-project-key.sh +55 -0
  30. package/plugins/inspector/scripts/lib/inspector-run.sh +305 -0
  31. package/plugins/inspector/scripts/lib/inspector-ulid.sh +45 -0
  32. package/test/bats/archivist-project-key.bats +79 -0
  33. package/test/bats/archivist-storage.bats +79 -0
  34. package/test/bats/compact-tracker.bats +125 -0
  35. package/test/bats/compass-config.bats +65 -0
  36. package/test/bats/compass-gate.bats +129 -0
  37. package/test/bats/compass-sanitizer.bats +69 -0
  38. package/test/bats/compass-symbolic-skip.bats +88 -0
  39. package/test/bats/compass-transcript.bats +80 -0
  40. package/test/bats/inspector-config.bats +118 -0
  41. package/test/bats/inspector-events.bats +156 -0
  42. package/test/bats/inspector-post-write-hook.bats +164 -0
  43. package/test/bats/inspector-project-key.bats +68 -0
  44. package/test/bats/inspector-ulid.bats +34 -0
  45. package/test/bats/onlooker-schema.bats +111 -0
  46. package/test/bats/prompt-rules.bats +98 -0
  47. package/test/bats/session-tracker.bats +260 -0
  48. package/test/bats/skill-usage-tracker.bats +63 -0
  49. package/test/bats/task-tracker.bats +102 -0
  50. package/test/bats/turn-tracker.bats +180 -0
  51. package/test/bats/validate-path.bats +125 -0
  52. package/test/bats/worktree-tracker.bats +167 -0
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bats
2
+
3
+ setup() {
4
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
5
+ setup_test_env
6
+
7
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/compass"
8
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
9
+ # shellcheck disable=SC1091
10
+ source "${PLUGIN_ROOT}/scripts/lib/compass-config.sh"
11
+ }
12
+
13
+ @test "compass is disabled by default" {
14
+ compass_config_load ""
15
+ run compass_config_enabled
16
+ [ "$status" -ne 0 ]
17
+ }
18
+
19
+ @test "user-level settings.json can enable compass" {
20
+ mkdir -p "${HOME}/.claude"
21
+ printf '%s\n' '{"compass":{"enabled":true}}' > "${HOME}/.claude/settings.json"
22
+ compass_config_load ""
23
+ run compass_config_enabled
24
+ [ "$status" -eq 0 ]
25
+ }
26
+
27
+ @test "repo-level settings.json overrides user-level" {
28
+ mkdir -p "${HOME}/.claude"
29
+ printf '%s\n' '{"compass":{"enabled":true}}' > "${HOME}/.claude/settings.json"
30
+ local repo="${BATS_TEST_TMPDIR}/repo"
31
+ mkdir -p "${repo}/.claude"
32
+ printf '%s\n' '{"compass":{"enabled":false}}' > "${repo}/.claude/settings.json"
33
+ compass_config_load "$repo"
34
+ run compass_config_enabled
35
+ [ "$status" -ne 0 ]
36
+ }
37
+
38
+ @test "shipped defaults survive a partial overlay" {
39
+ mkdir -p "${HOME}/.claude"
40
+ printf '%s\n' \
41
+ '{"compass":{"enabled":true,"evaluator":{"n":7}}}' \
42
+ > "${HOME}/.claude/settings.json"
43
+ compass_config_load ""
44
+ # Overlay key is picked up.
45
+ [ "$(compass_config_get '.compass.evaluator.n')" = "7" ]
46
+ # Defaults under the same parent key survive the deep merge.
47
+ [ "$(compass_config_get '.compass.evaluator.temperature')" = "0.3" ]
48
+ [ "$(compass_config_get '.compass.confidence_threshold')" = "0.65" ]
49
+ [ "$(compass_config_get '.compass.stddev_threshold')" = "0.2" ]
50
+ }
51
+
52
+ @test "config_get_json returns skip_globs array" {
53
+ compass_config_load ""
54
+ run compass_config_get_json '.compass.skip_globs'
55
+ [ "$status" -eq 0 ]
56
+ printf '%s' "$output" \
57
+ | jq -e 'index("**/*.lock") != null and index("**/.git/**") != null' >/dev/null
58
+ }
59
+
60
+ @test "transcript block no longer carries the obsolete transcript_max_age_seconds knob" {
61
+ compass_config_load ""
62
+ # Future readers should not find the removed knob — see ADR-001 (read by
63
+ # transcript_path from hook JSON, no event-log fallback).
64
+ [ -z "$(compass_config_get '.compass.transcript.transcript_max_age_seconds')" ]
65
+ }
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Covers compass_run_gate's pre-evaluator rules: skip sentinel,
4
+ # skip_globs, dir+stem cooldown, turn budget, context minimum. These are
5
+ # the cheap gating steps that run before any LLM call.
6
+
7
+ setup() {
8
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
9
+ setup_test_env
10
+
11
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/compass"
12
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
13
+
14
+ # Source the libs the gate depends on.
15
+ # shellcheck disable=SC1091
16
+ source "${PLUGIN_ROOT}/scripts/lib/compass-config.sh"
17
+ # shellcheck disable=SC1091
18
+ source "${PLUGIN_ROOT}/scripts/lib/compass-events.sh"
19
+ # shellcheck disable=SC1091
20
+ source "${PLUGIN_ROOT}/scripts/lib/compass-sanitizer.sh"
21
+ # shellcheck disable=SC1091
22
+ source "${PLUGIN_ROOT}/scripts/lib/compass-transcript.sh"
23
+ # shellcheck disable=SC1091
24
+ source "${PLUGIN_ROOT}/scripts/lib/compass-evaluator.sh"
25
+ # shellcheck disable=SC1091
26
+ source "${PLUGIN_ROOT}/scripts/lib/compass-gate.sh"
27
+
28
+ # Enable compass and load config.
29
+ mkdir -p "${HOME}/.claude"
30
+ printf '%s\n' '{"compass":{"enabled":true}}' > "${HOME}/.claude/settings.json"
31
+ compass_config_load ""
32
+
33
+ # Seed a session-state file so the gate's state lookups succeed.
34
+ export SESSION_ID="test-session-bats"
35
+ mkdir -p "${ONLOOKER_DIR}/compass/sessions"
36
+ cat > "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json" <<-EOF
37
+ {
38
+ "session_id": "${SESSION_ID}",
39
+ "turn_check_count": 0,
40
+ "cooldown": [],
41
+ "circuit_breaker": {"state":"closed","consecutive_failures":0,"opened_at":null}
42
+ }
43
+ EOF
44
+
45
+ # Stub compass_evaluate so the gate never shells out to claude -p
46
+ # during tests. The stub always reports a pass — tests that need to
47
+ # verify pre-evaluator rules check that the evaluator is short-circuited
48
+ # before it would run.
49
+ compass_evaluate() {
50
+ printf '{"decision":"pass","confidence":0.95,"stddev":0.03,"primary_concern":"none","rationale":"stub","sample_count":5}'
51
+ return 0
52
+ }
53
+ export -f compass_evaluate
54
+ }
55
+
56
+ @test "skip sentinel [compass:skip] in file path lets the write through" {
57
+ run compass_run_gate "Write" "/tmp/foo[compass:skip].txt" "write" \
58
+ "$(printf 'x%.0s' {1..200})" "$SESSION_ID" "" ""
59
+ [ "$status" -eq 0 ]
60
+ # Nothing written to stdout: no block decision.
61
+ [ -z "$output" ]
62
+ }
63
+
64
+ @test "skip sentinel [compass:skip] in context lets the write through" {
65
+ local ctx
66
+ ctx="[compass:skip] $(printf 'y%.0s' {1..200})"
67
+ run compass_run_gate "Write" "/tmp/foo.txt" "write" "$ctx" "$SESSION_ID" "" ""
68
+ [ "$status" -eq 0 ]
69
+ [ -z "$output" ]
70
+ }
71
+
72
+ @test "skip_glob match (*.lock) lets the write through" {
73
+ run compass_run_gate "Write" "/tmp/package-lock.lock" "write" \
74
+ "$(printf 'x%.0s' {1..200})" "$SESSION_ID" "" ""
75
+ [ "$status" -eq 0 ]
76
+ [ -z "$output" ]
77
+ }
78
+
79
+ @test "writes under .git/ are skipped" {
80
+ run compass_run_gate "Write" "/tmp/repo/.git/HEAD" "write" \
81
+ "$(printf 'x%.0s' {1..200})" "$SESSION_ID" "" ""
82
+ [ "$status" -eq 0 ]
83
+ [ -z "$output" ]
84
+ }
85
+
86
+ @test "context shorter than min_context_chars is skipped (insufficient_context)" {
87
+ run compass_run_gate "Write" "/tmp/short.txt" "write" \
88
+ "tiny" "$SESSION_ID" "" ""
89
+ [ "$status" -eq 0 ]
90
+ [ -z "$output" ]
91
+ }
92
+
93
+ @test "exhausted turn budget skips subsequent checks" {
94
+ # Force the budget to be already maxed out.
95
+ jq '.turn_check_count = 99' \
96
+ "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json" \
97
+ > "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json.new"
98
+ mv "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json.new" \
99
+ "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json"
100
+ run compass_run_gate "Write" "/tmp/over-budget.txt" "write" \
101
+ "$(printf 'x%.0s' {1..200})" "$SESSION_ID" "" ""
102
+ [ "$status" -eq 0 ]
103
+ [ -z "$output" ]
104
+ }
105
+
106
+ @test "dir+stem cooldown skips a re-write of the same file" {
107
+ # Pre-seed the cooldown table with a fresh entry.
108
+ local now
109
+ now=$(date +%s)
110
+ jq --argjson ts "$now" '
111
+ .cooldown = [{"identity":"/tmp/cool/foo","path":"/tmp/cool/foo.txt","ts":$ts}]
112
+ ' "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json" \
113
+ > "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json.new"
114
+ mv "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json.new" \
115
+ "${ONLOOKER_DIR}/compass/sessions/${SESSION_ID}.json"
116
+ run compass_run_gate "Write" "/tmp/cool/foo.txt" "write" \
117
+ "$(printf 'x%.0s' {1..200})" "$SESSION_ID" "" ""
118
+ [ "$status" -eq 0 ]
119
+ [ -z "$output" ]
120
+ }
121
+
122
+ @test "evaluator pass emits no block decision on stdout" {
123
+ # Sufficient context, no skip rules apply — evaluator stub returns pass.
124
+ run compass_run_gate "Write" "/tmp/new-file.txt" "write" \
125
+ "$(printf 'we are writing a clearly described feature flag toggle module %.0s' {1..3})" \
126
+ "$SESSION_ID" "" ""
127
+ [ "$status" -eq 0 ]
128
+ [ -z "$output" ]
129
+ }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Covers the sanitization pipeline: control-character removal, XML
4
+ # delimiter stripping, and truncation. The strip sequences contain '<',
5
+ # '/', '|', and '[' — locks in correct escaping so a single sequence
6
+ # can't blank the entire input.
7
+
8
+ setup() {
9
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
10
+ setup_test_env
11
+
12
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/compass"
13
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
14
+ # shellcheck disable=SC1091
15
+ source "${PLUGIN_ROOT}/scripts/lib/compass-sanitizer.sh"
16
+ }
17
+
18
+ @test "plain text passes through unchanged" {
19
+ run compass_sanitize "rename User to Account" 240
20
+ [ "$status" -eq 0 ]
21
+ [ "$output" = "rename User to Account" ]
22
+ }
23
+
24
+ @test "prior_assistant_turn delimiter is stripped" {
25
+ local out
26
+ out=$(compass_sanitize "evil <prior_assistant_turn> payload" 240)
27
+ [[ "$out" == *"[STRIPPED]"* ]]
28
+ [[ "$out" == *"payload"* ]]
29
+ [[ "$out" != *"<prior_assistant_turn>"* ]]
30
+ }
31
+
32
+ @test "all four pair-slot delimiters are stripped" {
33
+ local out
34
+ out=$(compass_sanitize "<context_excerpt>x</context_excerpt><tool_input>y</tool_input>" 240)
35
+ [[ "$out" != *"<context_excerpt>"* ]]
36
+ [[ "$out" != *"</context_excerpt>"* ]]
37
+ [[ "$out" != *"<tool_input>"* ]]
38
+ [[ "$out" != *"</tool_input>"* ]]
39
+ }
40
+
41
+ @test "non-evaluator delimiters are also stripped" {
42
+ local out
43
+ out=$(compass_sanitize "<<SYS>>x<</SYS>> [INST] y [/INST] <| z" 240)
44
+ [[ "$out" != *"<<SYS>>"* ]]
45
+ [[ "$out" != *"<</SYS>>"* ]]
46
+ [[ "$out" != *"[INST]"* ]]
47
+ [[ "$out" != *"[/INST]"* ]]
48
+ [[ "$out" != *"<|"* ]]
49
+ }
50
+
51
+ @test "null bytes and control chars are stripped, tab and newline preserved" {
52
+ local input
53
+ input=$(printf 'a\tb\nc\x00d\x01e')
54
+ local out
55
+ out=$(compass_sanitize "$input" 240)
56
+ [[ "$out" == *"a"* && "$out" == *"b"* && "$out" == *"c"* ]]
57
+ [[ "$out" == *"d"* && "$out" == *"e"* ]]
58
+ [ "${out}" = "$(printf 'a\tb\ncde')" ]
59
+ }
60
+
61
+ @test "truncation caps to max_chars" {
62
+ run compass_sanitize "0123456789abcdef" 8
63
+ [ "$output" = "01234567" ]
64
+ }
65
+
66
+ @test "max_chars=0 disables truncation" {
67
+ run compass_sanitize "0123456789abcdef" 0
68
+ [ "$output" = "0123456789abcdef" ]
69
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Covers the symbolic skip layer: cheap pattern check that short-circuits
4
+ # to a pass when the prior assistant turn is an enumerated question and
5
+ # the current context is a clean option reference. See ADR-001.
6
+
7
+ setup() {
8
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
9
+ setup_test_env
10
+
11
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/compass"
12
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
13
+
14
+ # Source the libs the gate depends on so _compass_symbolic_skip is in scope.
15
+ # shellcheck disable=SC1091
16
+ source "${PLUGIN_ROOT}/scripts/lib/compass-config.sh"
17
+ # shellcheck disable=SC1091
18
+ source "${PLUGIN_ROOT}/scripts/lib/compass-events.sh"
19
+ # shellcheck disable=SC1091
20
+ source "${PLUGIN_ROOT}/scripts/lib/compass-sanitizer.sh"
21
+ # shellcheck disable=SC1091
22
+ source "${PLUGIN_ROOT}/scripts/lib/compass-transcript.sh"
23
+ # shellcheck disable=SC1091
24
+ source "${PLUGIN_ROOT}/scripts/lib/compass-evaluator.sh"
25
+ # shellcheck disable=SC1091
26
+ source "${PLUGIN_ROOT}/scripts/lib/compass-gate.sh"
27
+ }
28
+
29
+ @test "skips when prior turn is enumerated question and reply is an ordinal" {
30
+ local prior='Which API should we touch?
31
+ 1. the internal one
32
+ 2. the public one'
33
+ run _compass_symbolic_skip "$prior" "the first one"
34
+ [ "$status" -eq 0 ]
35
+ }
36
+
37
+ @test "skips on a bare digit option reference" {
38
+ local prior='1. test_one
39
+ 2. test_two
40
+ Which one?'
41
+ run _compass_symbolic_skip "$prior" "2"
42
+ [ "$status" -eq 0 ]
43
+ }
44
+
45
+ @test "skips on a clean affirmation" {
46
+ local prior='1. left
47
+ 2. right
48
+ which side?'
49
+ run _compass_symbolic_skip "$prior" "both"
50
+ [ "$status" -eq 0 ]
51
+ }
52
+
53
+ @test "does NOT skip when the prior turn has no enumerated list" {
54
+ local prior='Should we proceed with the refactor?'
55
+ run _compass_symbolic_skip "$prior" "yes"
56
+ [ "$status" -ne 0 ]
57
+ }
58
+
59
+ @test "does NOT skip when the prior turn has no question mark" {
60
+ local prior='Here are the options:
61
+ 1. A
62
+ 2. B'
63
+ run _compass_symbolic_skip "$prior" "1"
64
+ [ "$status" -ne 0 ]
65
+ }
66
+
67
+ @test "does NOT skip when prior turn is empty" {
68
+ run _compass_symbolic_skip "" "yes"
69
+ [ "$status" -ne 0 ]
70
+ }
71
+
72
+ @test "does NOT skip on a hedged affirmation (qualifier clause present)" {
73
+ skip "hedged-qualifier rejection not yet implemented in _compass_symbolic_skip"
74
+ local prior='1. delete now
75
+ 2. archive first
76
+ Which one?'
77
+ # A qualifier clause means the reply is not a clean option reference.
78
+ run _compass_symbolic_skip "$prior" "both, but only if it's easy"
79
+ [ "$status" -ne 0 ]
80
+ }
81
+
82
+ @test "does NOT skip on a free-form reply with no option shape" {
83
+ local prior='1. one
84
+ 2. two
85
+ Pick?'
86
+ run _compass_symbolic_skip "$prior" "Actually I think we should refactor the whole thing"
87
+ [ "$status" -ne 0 ]
88
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Covers the prior-turn reader: pulls the most recent assistant turn from
4
+ # the session transcript JSONL identified by transcript_path. Source of
5
+ # truth is the hook JSON payload (ADR-001); the event log is not a
6
+ # fallback for assistant content.
7
+
8
+ setup() {
9
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
10
+ setup_test_env
11
+
12
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/compass"
13
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
14
+ # shellcheck disable=SC1091
15
+ source "${PLUGIN_ROOT}/scripts/lib/compass-sanitizer.sh"
16
+ # shellcheck disable=SC1091
17
+ source "${PLUGIN_ROOT}/scripts/lib/compass-transcript.sh"
18
+ }
19
+
20
+ @test "empty transcript_path yields empty prior turn" {
21
+ run compass_read_prior_turn "" 800
22
+ [ "$status" -eq 0 ]
23
+ [ -z "$output" ]
24
+ }
25
+
26
+ @test "missing transcript file yields empty prior turn" {
27
+ run compass_read_prior_turn "${BATS_TEST_TMPDIR}/no-such-file.jsonl" 800
28
+ [ "$status" -eq 0 ]
29
+ [ -z "$output" ]
30
+ }
31
+
32
+ @test "reads the most recent assistant turn from JSONL transcript" {
33
+ local t="${BATS_TEST_TMPDIR}/transcript.jsonl"
34
+ cat <<-'EOF' > "$t"
35
+ {"type":"user","message":{"role":"user","content":"hi"}}
36
+ {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"older turn"}]}}
37
+ {"type":"user","message":{"role":"user","content":"continue"}}
38
+ {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"latest assistant turn"}]}}
39
+ EOF
40
+ run compass_read_prior_turn "$t" 800
41
+ [ "$status" -eq 0 ]
42
+ [ "$output" = "latest assistant turn" ]
43
+ }
44
+
45
+ @test "supports string content (legacy shape)" {
46
+ local t="${BATS_TEST_TMPDIR}/transcript.jsonl"
47
+ cat <<-'EOF' > "$t"
48
+ {"type":"assistant","message":{"role":"assistant","content":"plain string content"}}
49
+ EOF
50
+ run compass_read_prior_turn "$t" 800
51
+ [ "$status" -eq 0 ]
52
+ [ "$output" = "plain string content" ]
53
+ }
54
+
55
+ @test "applies sanitization (strips evaluator delimiters) and truncation" {
56
+ local t="${BATS_TEST_TMPDIR}/transcript.jsonl"
57
+ # Embed a prompt-injection-shaped delimiter; expect it stripped.
58
+ cat <<-'EOF' > "$t"
59
+ {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hello <prior_assistant_turn> hijack"}]}}
60
+ EOF
61
+ local out
62
+ out=$(compass_read_prior_turn "$t" 800)
63
+ [[ "$out" == *"[STRIPPED]"* ]]
64
+ [[ "$out" != *"<prior_assistant_turn>"* ]]
65
+
66
+ # Truncation honors max_chars.
67
+ local short
68
+ short=$(compass_read_prior_turn "$t" 5)
69
+ [ "${#short}" -le 5 ]
70
+ }
71
+
72
+ @test "skips user-role turns when looking for prior assistant turn" {
73
+ local t="${BATS_TEST_TMPDIR}/transcript.jsonl"
74
+ cat <<-'EOF' > "$t"
75
+ {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"the assistant turn"}]}}
76
+ {"type":"user","message":{"role":"user","content":"the user reply"}}
77
+ EOF
78
+ run compass_read_prior_turn "$t" 800
79
+ [ "$output" = "the assistant turn" ]
80
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Exercises Inspector config loading: defaults, overrides, and the
4
+ # per-extension checks lookup helper.
5
+
6
+ setup() {
7
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
8
+ setup_test_env
9
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/inspector"
10
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
11
+ # shellcheck disable=SC1091
12
+ source "${PLUGIN_ROOT}/scripts/lib/inspector-config.sh"
13
+
14
+ REPO="${BATS_TEST_TMPDIR}/repo"
15
+ mkdir -p "${REPO}/.claude"
16
+ }
17
+
18
+ @test "disabled by default" {
19
+ inspector_config_load "$REPO"
20
+ run inspector_config_enabled
21
+ [ "$status" -ne 0 ]
22
+ }
23
+
24
+ @test "enabled when settings opt in" {
25
+ printf '%s\n' '{"inspector":{"enabled":true}}' >"${REPO}/.claude/settings.json"
26
+ inspector_config_load "$REPO"
27
+ run inspector_config_enabled
28
+ [ "$status" -eq 0 ]
29
+ }
30
+
31
+ @test "default timeout_seconds_per_check is 10" {
32
+ inspector_config_load "$REPO"
33
+ [ "$(inspector_config_timeout_per_check)" = "10" ]
34
+ }
35
+
36
+ @test "timeout_seconds_per_check override is honored" {
37
+ printf '%s\n' '{"inspector":{"timeout_seconds_per_check":3}}' >"${REPO}/.claude/settings.json"
38
+ inspector_config_load "$REPO"
39
+ [ "$(inspector_config_timeout_per_check)" = "3" ]
40
+ }
41
+
42
+ @test "default total_timeout_seconds is 30" {
43
+ inspector_config_load "$REPO"
44
+ [ "$(inspector_config_total_timeout)" = "30" ]
45
+ }
46
+
47
+ @test "default output_excerpt_max_bytes is 4096" {
48
+ inspector_config_load "$REPO"
49
+ [ "$(inspector_config_output_excerpt_max_bytes)" = "4096" ]
50
+ }
51
+
52
+ @test "show_clean_runs is false by default" {
53
+ inspector_config_load "$REPO"
54
+ run inspector_config_show_clean_runs
55
+ [ "$status" -ne 0 ]
56
+ }
57
+
58
+ @test "show_clean_runs flips on when set" {
59
+ printf '%s\n' '{"inspector":{"show_clean_runs":true}}' >"${REPO}/.claude/settings.json"
60
+ inspector_config_load "$REPO"
61
+ run inspector_config_show_clean_runs
62
+ [ "$status" -eq 0 ]
63
+ }
64
+
65
+ @test "default exclude_paths includes node_modules and dist" {
66
+ inspector_config_load "$REPO"
67
+ local excludes
68
+ excludes=$(inspector_config_exclude_paths)
69
+ echo "$excludes" | jq -e 'index("node_modules")' >/dev/null
70
+ echo "$excludes" | jq -e 'index("dist")' >/dev/null
71
+ }
72
+
73
+ @test "exclude_paths is fully replaced by repo settings (not merged)" {
74
+ printf '%s\n' '{"inspector":{"exclude_paths":["only-this"]}}' >"${REPO}/.claude/settings.json"
75
+ inspector_config_load "$REPO"
76
+ local excludes
77
+ excludes=$(inspector_config_exclude_paths)
78
+ [ "$(echo "$excludes" | jq 'length')" = "1" ]
79
+ echo "$excludes" | jq -e 'index("only-this")' >/dev/null
80
+ }
81
+
82
+ @test "checks_for_extension returns [] when extension is unconfigured" {
83
+ inspector_config_load "$REPO"
84
+ [ "$(inspector_config_checks_for_extension '.ts')" = "[]" ]
85
+ }
86
+
87
+ @test "checks_for_extension returns the configured object form" {
88
+ cat >"${REPO}/.claude/settings.json" <<'EOF'
89
+ { "inspector": { "checks": { ".ts": [
90
+ { "name": "biome", "kind": "lint", "argv": ["biome", "check", "${file}"] }
91
+ ] } } }
92
+ EOF
93
+ inspector_config_load "$REPO"
94
+ local checks
95
+ checks=$(inspector_config_checks_for_extension '.ts')
96
+ [ "$(echo "$checks" | jq 'length')" = "1" ]
97
+ [ "$(echo "$checks" | jq -r '.[0].name')" = "biome" ]
98
+ [ "$(echo "$checks" | jq -r '.[0].kind')" = "lint" ]
99
+ }
100
+
101
+ @test "checks_for_extension normalizes bare argv arrays into objects" {
102
+ cat >"${REPO}/.claude/settings.json" <<'EOF'
103
+ { "inspector": { "checks": { ".sh": [
104
+ ["shellcheck", "${file}"]
105
+ ] } } }
106
+ EOF
107
+ inspector_config_load "$REPO"
108
+ local checks
109
+ checks=$(inspector_config_checks_for_extension '.sh')
110
+ [ "$(echo "$checks" | jq -r '.[0].name')" = "shellcheck" ]
111
+ [ "$(echo "$checks" | jq -r '.[0].kind')" = "lint" ]
112
+ [ "$(echo "$checks" | jq -r '.[0].argv | length')" = "2" ]
113
+ }
114
+
115
+ @test "checks_for_extension returns [] for an empty extension" {
116
+ inspector_config_load "$REPO"
117
+ [ "$(inspector_config_checks_for_extension '')" = "[]" ]
118
+ }