@onlooker-community/ecosystem 0.28.1 → 0.29.1

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 (56) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.release-please-manifest.json +4 -3
  4. package/CHANGELOG.md +14 -0
  5. package/CLAUDE.md +2 -0
  6. package/README.md +114 -13
  7. package/docs/plugin-catalog.md +125 -0
  8. package/package.json +3 -3
  9. package/plugins/compass/.claude-plugin/plugin.json +1 -1
  10. package/plugins/compass/CHANGELOG.md +7 -0
  11. package/plugins/compass/README.md +1 -3
  12. package/plugins/compass/config.json +1 -2
  13. package/plugins/compass/docs/design.md +1 -2
  14. package/plugins/compass/scripts/hooks/compass-bash-gate.sh +8 -1
  15. package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +8 -1
  16. package/plugins/compass/scripts/hooks/compass-record-write.sh +5 -0
  17. package/plugins/compass/scripts/hooks/compass-session-start.sh +0 -8
  18. package/plugins/compass/scripts/lib/compass-evaluator.sh +58 -98
  19. package/plugins/compass/scripts/lib/compass-gate.sh +15 -18
  20. package/plugins/compass/scripts/lib/compass-sanitizer.sh +4 -4
  21. package/plugins/compass/scripts/lib/compass-transcript.sh +79 -112
  22. package/plugins/inspector/.claude-plugin/plugin.json +14 -0
  23. package/plugins/inspector/CHANGELOG.md +8 -0
  24. package/plugins/inspector/README.md +155 -0
  25. package/plugins/inspector/config.json +25 -0
  26. package/plugins/inspector/docs/design.md +286 -0
  27. package/plugins/inspector/hooks/hooks.json +33 -0
  28. package/plugins/inspector/scripts/hooks/inspector-post-write.sh +124 -0
  29. package/plugins/inspector/scripts/lib/inspector-config.sh +108 -0
  30. package/plugins/inspector/scripts/lib/inspector-events.sh +82 -0
  31. package/plugins/inspector/scripts/lib/inspector-project-key.sh +55 -0
  32. package/plugins/inspector/scripts/lib/inspector-run.sh +305 -0
  33. package/plugins/inspector/scripts/lib/inspector-ulid.sh +45 -0
  34. package/release-please-config.json +17 -1
  35. package/test/bats/archivist-project-key.bats +79 -0
  36. package/test/bats/archivist-storage.bats +79 -0
  37. package/test/bats/compact-tracker.bats +125 -0
  38. package/test/bats/compass-config.bats +65 -0
  39. package/test/bats/compass-gate.bats +129 -0
  40. package/test/bats/compass-sanitizer.bats +69 -0
  41. package/test/bats/compass-symbolic-skip.bats +88 -0
  42. package/test/bats/compass-transcript.bats +80 -0
  43. package/test/bats/inspector-config.bats +118 -0
  44. package/test/bats/inspector-events.bats +156 -0
  45. package/test/bats/inspector-post-write-hook.bats +164 -0
  46. package/test/bats/inspector-project-key.bats +68 -0
  47. package/test/bats/inspector-ulid.bats +34 -0
  48. package/test/bats/librarian-session-end.bats +8 -1
  49. package/test/bats/onlooker-schema.bats +111 -0
  50. package/test/bats/prompt-rules.bats +98 -0
  51. package/test/bats/session-tracker.bats +260 -0
  52. package/test/bats/skill-usage-tracker.bats +63 -0
  53. package/test/bats/task-tracker.bats +102 -0
  54. package/test/bats/turn-tracker.bats +180 -0
  55. package/test/bats/validate-path.bats +125 -0
  56. package/test/bats/worktree-tracker.bats +167 -0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # inspector-config.sh — load and query Inspector configuration.
3
+ #
4
+ # Merges three layers in precedence order (later wins):
5
+ # 1. plugins/inspector/config.json (plugin defaults)
6
+ # 2. ~/.claude/settings.json (.inspector subtree)
7
+ # 3. <repo>/.claude/settings.json (.inspector subtree)
8
+ #
9
+ # Usage:
10
+ # inspector_config_load <repo_root>
11
+ # inspector_config_enabled
12
+ # inspector_config_get ".inspector.timeout_seconds_per_check"
13
+ # inspector_config_get_json ".inspector.exclude_paths"
14
+ # inspector_config_checks_for_extension ".ts"
15
+
16
+ _INSPECTOR_CONFIG=""
17
+ _INSPECTOR_PLUGIN_CONFIG=""
18
+
19
+ inspector_config_load() {
20
+ local repo_root="${1:-}"
21
+ local plugin_dir
22
+ plugin_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
23
+ local plugin_config="$plugin_dir/config.json"
24
+
25
+ _INSPECTOR_PLUGIN_CONFIG="{}"
26
+ if [[ -f "$plugin_config" ]]; then
27
+ _INSPECTOR_PLUGIN_CONFIG=$(cat "$plugin_config")
28
+ fi
29
+
30
+ local home_settings="{}"
31
+ if [[ -f "$HOME/.claude/settings.json" ]]; then
32
+ home_settings=$(cat "$HOME/.claude/settings.json")
33
+ fi
34
+
35
+ local repo_settings="{}"
36
+ if [[ -n "$repo_root" && -f "$repo_root/.claude/settings.json" ]]; then
37
+ repo_settings=$(cat "$repo_root/.claude/settings.json")
38
+ fi
39
+
40
+ _INSPECTOR_CONFIG=$(jq -n \
41
+ --argjson plugin "$_INSPECTOR_PLUGIN_CONFIG" \
42
+ --argjson home "$home_settings" \
43
+ --argjson repo "$repo_settings" \
44
+ '$plugin * {"inspector": (($plugin.inspector // {}) * ($home.inspector // {}) * ($repo.inspector // {}))}')
45
+ }
46
+
47
+ inspector_config_get() {
48
+ local path="${1:-}"
49
+ printf '%s' "$_INSPECTOR_CONFIG" | jq -r "$path // empty" 2>/dev/null
50
+ }
51
+
52
+ inspector_config_get_json() {
53
+ local path="${1:-}"
54
+ printf '%s' "$_INSPECTOR_CONFIG" | jq -c "$path // empty" 2>/dev/null
55
+ }
56
+
57
+ inspector_config_enabled() {
58
+ local v
59
+ v=$(inspector_config_get '.inspector.enabled')
60
+ [[ "$v" == "true" ]]
61
+ }
62
+
63
+ inspector_config_show_clean_runs() {
64
+ local v
65
+ v=$(inspector_config_get '.inspector.show_clean_runs')
66
+ [[ "$v" == "true" ]]
67
+ }
68
+
69
+ inspector_config_timeout_per_check() {
70
+ local v
71
+ v=$(inspector_config_get '.inspector.timeout_seconds_per_check')
72
+ printf '%s' "${v:-10}"
73
+ }
74
+
75
+ inspector_config_total_timeout() {
76
+ local v
77
+ v=$(inspector_config_get '.inspector.total_timeout_seconds')
78
+ printf '%s' "${v:-30}"
79
+ }
80
+
81
+ inspector_config_output_excerpt_max_bytes() {
82
+ local v
83
+ v=$(inspector_config_get '.inspector.output_excerpt_max_bytes')
84
+ printf '%s' "${v:-4096}"
85
+ }
86
+
87
+ inspector_config_exclude_paths() {
88
+ inspector_config_get_json '.inspector.exclude_paths // []'
89
+ }
90
+
91
+ # Emits a JSON array of {name, argv, kind} objects for the given file extension
92
+ # (including the leading dot). Returns an empty array when no checks are
93
+ # configured for the extension.
94
+ inspector_config_checks_for_extension() {
95
+ local ext="${1:-}"
96
+ [[ -z "$ext" ]] && { printf '[]'; return; }
97
+ printf '%s' "$_INSPECTOR_CONFIG" | jq -c --arg ext "$ext" '
98
+ (.inspector.checks // {}) as $checks
99
+ | ($checks[$ext] // [])
100
+ | map(
101
+ if type == "array" then
102
+ { name: (.[0] // "check"), argv: ., kind: "lint" }
103
+ else
104
+ { name: (.name // "check"), argv: (.argv // []), kind: (.kind // "lint") }
105
+ end
106
+ )
107
+ ' 2>/dev/null || printf '[]'
108
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # inspector-events.sh — emit inspector.* events to the canonical event log.
3
+ #
4
+ # Thin wrapper around onlooker-event.mjs. Validation failures are logged to
5
+ # stderr and do not abort the caller — Inspector is advisory.
6
+ #
7
+ # Usage:
8
+ # inspector_emit_event "inspector.check.passed" '{"file_path":"...","tool_name":"Edit",...}'
9
+
10
+ _INSPECTOR_PLUGIN_NAME="inspector"
11
+
12
+ _inspector_event_js_path() {
13
+ if [[ -n "${_ONLOOKER_EVENT_JS:-}" && -f "$_ONLOOKER_EVENT_JS" ]]; then
14
+ printf '%s' "$_ONLOOKER_EVENT_JS"
15
+ return 0
16
+ fi
17
+ local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
18
+ local candidates=(
19
+ "${plugin_root}/scripts/lib/onlooker-event.mjs"
20
+ "${plugin_root}/../../scripts/lib/onlooker-event.mjs"
21
+ )
22
+ local c
23
+ for c in "${candidates[@]}"; do
24
+ [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
25
+ done
26
+ return 1
27
+ }
28
+
29
+ _inspector_session_id() {
30
+ if [[ -n "${_HOOK_SESSION_ID:-}" ]]; then
31
+ printf '%s' "$_HOOK_SESSION_ID"
32
+ return 0
33
+ fi
34
+ if [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then
35
+ printf '%s' "$CLAUDE_SESSION_ID"
36
+ return 0
37
+ fi
38
+ printf 'unknown'
39
+ }
40
+
41
+ inspector_emit_event() {
42
+ local event_type="${1:-}"
43
+ local payload="${2:-}"
44
+ [[ -z "$event_type" || -z "$payload" ]] && return 1
45
+
46
+ local event_js
47
+ event_js=$(_inspector_event_js_path) || {
48
+ printf 'inspector-events: cannot locate onlooker-event.mjs\n' >&2
49
+ return 1
50
+ }
51
+
52
+ local session_id
53
+ session_id=$(_inspector_session_id)
54
+
55
+ local params
56
+ params=$(jq -n \
57
+ --arg plugin "$_INSPECTOR_PLUGIN_NAME" \
58
+ --arg sid "$session_id" \
59
+ --arg type "$event_type" \
60
+ --argjson payload "$payload" \
61
+ '{"plugin":$plugin,"session_id":$sid,"event_type":$type,"payload":$payload}')
62
+
63
+ local stderr_file
64
+ stderr_file=$(mktemp -t inspector-event-err.XXXXXX 2>/dev/null) \
65
+ || stderr_file="/tmp/inspector-event-err.$$"
66
+
67
+ local event
68
+ event=$(printf '%s' "$params" \
69
+ | ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}" \
70
+ ONLOOKER_PLUGIN_NAME="$_INSPECTOR_PLUGIN_NAME" \
71
+ node "$event_js" emit 2>"$stderr_file") || {
72
+ printf 'inspector-events: schema validation failed for %s\n' "$event_type" >&2
73
+ [[ -s "$stderr_file" ]] && cat "$stderr_file" >&2
74
+ rm -f "$stderr_file"
75
+ return 1
76
+ }
77
+ rm -f "$stderr_file"
78
+
79
+ local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
80
+ mkdir -p "$(dirname "$log_path")" 2>/dev/null || return 1
81
+ printf '%s\n' "$event" >>"$log_path"
82
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # inspector-project-key.sh — stable 12-char hex project key.
3
+ #
4
+ # Derives a key that survives repo renames, clones, and worktrees.
5
+ # Algorithm:
6
+ # 1. git remote get-url origin → sha256("remote:" + url)[0:12]
7
+ # 2. Fallback: git rev-parse --show-toplevel → sha256("root:" + path)[0:12]
8
+ # 3. Non-git: sha256("cwd:" + pwd)[0:12]
9
+ #
10
+ # Usage:
11
+ # key=$(inspector_project_key <cwd>)
12
+ # root=$(inspector_project_repo_root <cwd>)
13
+
14
+ _inspector_sha256_first12() {
15
+ local input="$1"
16
+ if command -v sha256sum &>/dev/null; then
17
+ printf '%s' "$input" | sha256sum | cut -c1-12
18
+ elif command -v shasum &>/dev/null; then
19
+ printf '%s' "$input" | shasum -a 256 | cut -c1-12
20
+ else
21
+ printf '%s' "$input" | python3 -c \
22
+ 'import sys,hashlib; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:12])'
23
+ fi
24
+ }
25
+
26
+ inspector_project_repo_root() {
27
+ local cwd="${1:-$(pwd)}"
28
+ local root
29
+ root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) && printf '%s' "$root" && return 0
30
+ printf '%s' "$cwd"
31
+ }
32
+
33
+ inspector_project_remote_url() {
34
+ local cwd="${1:-$(pwd)}"
35
+ git -C "$cwd" remote get-url origin 2>/dev/null || true
36
+ }
37
+
38
+ inspector_project_key() {
39
+ local cwd="${1:-$(pwd)}"
40
+ local remote
41
+ remote=$(inspector_project_remote_url "$cwd")
42
+ if [[ -n "$remote" ]]; then
43
+ _inspector_sha256_first12 "remote:${remote}"
44
+ return 0
45
+ fi
46
+
47
+ local root
48
+ root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
49
+ if [[ -n "$root" ]]; then
50
+ _inspector_sha256_first12 "root:${root}"
51
+ return 0
52
+ fi
53
+
54
+ _inspector_sha256_first12 "cwd:${cwd}"
55
+ }
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env bash
2
+ # inspector-run.sh — execute the configured checks for a single touched file.
3
+ #
4
+ # Reads: $INSPECTOR_FILE, $INSPECTOR_FILE_RELATIVE, $INSPECTOR_REPO_ROOT,
5
+ # $INSPECTOR_PROJECT_KEY, $INSPECTOR_TOOL_NAME
6
+ # Emits: inspector.check.passed / .failed / .skipped (per check), then
7
+ # inspector.run.completed (once).
8
+ # Writes: a compact per-file summary to stdout, intended to be shown to the
9
+ # agent as PostToolUse additional context.
10
+
11
+ set -uo pipefail
12
+
13
+ _inspector_now_ms() {
14
+ if date +%s%3N &>/dev/null && [[ "$(date +%s%3N)" =~ ^[0-9]{13}$ ]]; then
15
+ date +%s%3N
16
+ else
17
+ python3 -c 'import time; print(int(time.time() * 1000))'
18
+ fi
19
+ }
20
+
21
+ # Substitute ${file}, ${file_relative}, ${repo_root} in each argv element.
22
+ _inspector_expand_argv() {
23
+ local raw_argv_json="$1"
24
+ jq -c \
25
+ --arg file "$INSPECTOR_FILE" \
26
+ --arg rel "$INSPECTOR_FILE_RELATIVE" \
27
+ --arg root "$INSPECTOR_REPO_ROOT" \
28
+ 'map(
29
+ gsub("\\$\\{file\\}"; $file)
30
+ | gsub("\\$\\{file_relative\\}"; $rel)
31
+ | gsub("\\$\\{repo_root\\}"; $root)
32
+ )' <<<"$raw_argv_json"
33
+ }
34
+
35
+ # Run a single check. Stdout: the captured combined output, truncated to
36
+ # output_excerpt_max_bytes. Exit code: the underlying command's exit, or 124 on
37
+ # timeout, or 127 when the command is not on PATH.
38
+ _inspector_invoke_check() {
39
+ local expanded_json="$1"
40
+ local timeout_s="$2"
41
+ local max_bytes="$3"
42
+
43
+ # Build argv array from JSON for safe execution.
44
+ # bash 3.2 (macOS default) has no `mapfile`; collect with a while-read loop.
45
+ local expanded_argv=()
46
+ local _line
47
+ while IFS= read -r _line; do
48
+ expanded_argv+=("$_line")
49
+ done < <(jq -r '.[]' <<<"$expanded_json")
50
+ local cmd="${expanded_argv[0]:-}"
51
+ if [[ -z "$cmd" ]]; then
52
+ printf 'inspector: empty argv\n'
53
+ return 127
54
+ fi
55
+ if ! command -v "$cmd" >/dev/null 2>&1; then
56
+ return 127
57
+ fi
58
+
59
+ local output_file rc=0
60
+ output_file=$(mktemp -t inspector-out.XXXXXX 2>/dev/null) \
61
+ || output_file="/tmp/inspector-out.$$"
62
+
63
+ if command -v timeout >/dev/null 2>&1; then
64
+ timeout "${timeout_s}s" "${expanded_argv[@]}" >"$output_file" 2>&1
65
+ rc=$?
66
+ else
67
+ "${expanded_argv[@]}" >"$output_file" 2>&1
68
+ rc=$?
69
+ fi
70
+
71
+ # Truncate output for the event payload and the agent-facing line.
72
+ local bytes=0
73
+ bytes=$(wc -c <"$output_file" 2>/dev/null || printf '0')
74
+ if (( bytes > max_bytes )); then
75
+ head -c "$max_bytes" "$output_file"
76
+ printf '\n…[truncated]\n'
77
+ else
78
+ cat "$output_file"
79
+ fi
80
+ rm -f "$output_file"
81
+ return "$rc"
82
+ }
83
+
84
+ # Count issues from output — best-effort. One non-empty, non-whitespace line per
85
+ # issue, ignoring trivial header/footer markers. Returns the literal string
86
+ # "null" when the output is empty or only whitespace.
87
+ _inspector_count_issues() {
88
+ local text="$1"
89
+ [[ -z "$text" ]] && { printf 'null'; return; }
90
+ local count
91
+ count=$(printf '%s' "$text" | grep -cE '^[^[:space:]]' || true)
92
+ if [[ -z "$count" || "$count" == "0" ]]; then
93
+ printf 'null'
94
+ else
95
+ printf '%s' "$count"
96
+ fi
97
+ }
98
+
99
+ # Public entrypoint. Iterates the configured checks for the file's extension.
100
+ inspector_run() {
101
+ local checks_json="${1:-[]}"
102
+ local timeout_per_check
103
+ timeout_per_check=$(inspector_config_timeout_per_check)
104
+ local total_timeout
105
+ total_timeout=$(inspector_config_total_timeout)
106
+ local max_bytes
107
+ max_bytes=$(inspector_config_output_excerpt_max_bytes)
108
+ local show_clean=0
109
+ inspector_config_show_clean_runs && show_clean=1
110
+
111
+ local check_count
112
+ check_count=$(jq 'length' <<<"$checks_json")
113
+
114
+ local run_start
115
+ run_start=$(_inspector_now_ms)
116
+ local passed=0 failed=0 skipped=0 ran=0
117
+
118
+ # Buffer agent-facing output so we only print the file header when at least
119
+ # one issue (or, when show_clean is set, one check) is worth reporting.
120
+ local agent_lines=()
121
+ local issues_seen=0
122
+
123
+ local i
124
+ for (( i = 0; i < check_count; i++ )); do
125
+ # Budget check before each run.
126
+ local now_ms
127
+ now_ms=$(_inspector_now_ms)
128
+ if (( (now_ms - run_start) >= (total_timeout * 1000) )); then
129
+ local rem
130
+ rem=$(jq -c --argjson i "$i" '.[$i:]' <<<"$checks_json")
131
+ local rem_count
132
+ rem_count=$(jq 'length' <<<"$rem")
133
+ local j
134
+ for (( j = 0; j < rem_count; j++ )); do
135
+ local r_name r_kind
136
+ r_name=$(jq -r --argjson j "$j" '.[$j].name // "check"' <<<"$rem")
137
+ r_kind=$(jq -r --argjson j "$j" '.[$j].kind // "lint"' <<<"$rem")
138
+ _inspector_emit_skipped "$r_name" "$r_kind" "total_budget_exhausted"
139
+ (( skipped++ ))
140
+ done
141
+ break
142
+ fi
143
+
144
+ local check_name check_kind argv_raw
145
+ check_name=$(jq -r --argjson i "$i" '.[$i].name' <<<"$checks_json")
146
+ check_kind=$(jq -r --argjson i "$i" '.[$i].kind' <<<"$checks_json")
147
+ argv_raw=$(jq -c --argjson i "$i" '.[$i].argv' <<<"$checks_json")
148
+ local argv_expanded
149
+ argv_expanded=$(_inspector_expand_argv "$argv_raw")
150
+
151
+ local check_start
152
+ check_start=$(_inspector_now_ms)
153
+ local output rc=0
154
+ output=$(_inspector_invoke_check "$argv_expanded" "$timeout_per_check" "$max_bytes")
155
+ rc=$?
156
+ local check_end
157
+ check_end=$(_inspector_now_ms)
158
+ local dur=$(( check_end - check_start ))
159
+
160
+ if (( rc == 127 )); then
161
+ # Tool not on PATH.
162
+ _inspector_emit_skipped "$check_name" "$check_kind" "tool_missing"
163
+ (( skipped++ ))
164
+ continue
165
+ fi
166
+
167
+ if (( rc == 124 )); then
168
+ # Timed out — emit skipped + a single agent-facing line.
169
+ _inspector_emit_skipped "$check_name" "$check_kind" "timeout"
170
+ agent_lines+=(" · $check_name timed out after ${timeout_per_check}s")
171
+ (( skipped++ ))
172
+ (( issues_seen++ ))
173
+ continue
174
+ fi
175
+
176
+ (( ran++ ))
177
+ if (( rc == 0 )); then
178
+ (( passed++ ))
179
+ _inspector_emit_passed "$check_name" "$check_kind" "$argv_expanded" "$dur"
180
+ if (( show_clean )); then
181
+ agent_lines+=(" ✓ $check_name (${dur}ms)")
182
+ fi
183
+ else
184
+ (( failed++ ))
185
+ local issue_count
186
+ issue_count=$(_inspector_count_issues "$output")
187
+ _inspector_emit_failed "$check_name" "$check_kind" "$argv_expanded" "$dur" "$rc" "$issue_count" "$output"
188
+ local issues_label="${issue_count}"
189
+ if [[ "$issues_label" == "null" ]]; then
190
+ issues_label="issues"
191
+ else
192
+ issues_label="${issues_label} issue(s)"
193
+ fi
194
+ agent_lines+=(" ✗ $check_name (${issues_label}, exit ${rc})")
195
+ # Append up to 6 issue lines for at-a-glance context.
196
+ local snippet
197
+ snippet=$(printf '%s\n' "$output" | grep -E '^[^[:space:]]' | head -n 6 || true)
198
+ while IFS= read -r line; do
199
+ [[ -z "$line" ]] && continue
200
+ agent_lines+=(" $line")
201
+ done <<<"$snippet"
202
+ (( issues_seen++ ))
203
+ fi
204
+ done
205
+
206
+ local run_end
207
+ run_end=$(_inspector_now_ms)
208
+ local run_dur=$(( run_end - run_start ))
209
+
210
+ _inspector_emit_run_completed "$ran" "$passed" "$failed" "$skipped" "$run_dur"
211
+
212
+ if (( issues_seen > 0 )) || (( show_clean && (passed + failed) > 0 )); then
213
+ printf 'inspector: %s\n' "$INSPECTOR_FILE_RELATIVE"
214
+ printf '%s\n' "${agent_lines[@]}"
215
+ fi
216
+ }
217
+
218
+ _inspector_emit_passed() {
219
+ local name="$1" kind="$2" argv_json="$3" dur="$4"
220
+ local payload
221
+ payload=$(jq -n \
222
+ --arg file "$INSPECTOR_FILE" \
223
+ --arg rel "$INSPECTOR_FILE_RELATIVE" \
224
+ --arg tool "$INSPECTOR_TOOL_NAME" \
225
+ --arg name "$name" \
226
+ --arg kind "$kind" \
227
+ --arg pk "$INSPECTOR_PROJECT_KEY" \
228
+ --argjson argv "$argv_json" \
229
+ --argjson dur "$dur" \
230
+ '{file_path:$file,file_path_relative:$rel,tool_name:$tool,check_name:$name,check_kind:$kind,argv:$argv,duration_ms:$dur,project_key:$pk}')
231
+ inspector_emit_event "inspector.check.passed" "$payload" || true
232
+ }
233
+
234
+ _inspector_emit_failed() {
235
+ local name="$1" kind="$2" argv_json="$3" dur="$4" rc="$5" issues="$6" output="$7"
236
+ local issues_arg
237
+ if [[ "$issues" == "null" ]]; then
238
+ issues_arg="null"
239
+ else
240
+ issues_arg="$issues"
241
+ fi
242
+ local truncated="false"
243
+ [[ "$output" == *"…[truncated]"* ]] && truncated="true"
244
+ local payload
245
+ payload=$(jq -n \
246
+ --arg file "$INSPECTOR_FILE" \
247
+ --arg rel "$INSPECTOR_FILE_RELATIVE" \
248
+ --arg tool "$INSPECTOR_TOOL_NAME" \
249
+ --arg name "$name" \
250
+ --arg kind "$kind" \
251
+ --arg pk "$INSPECTOR_PROJECT_KEY" \
252
+ --arg output "$output" \
253
+ --argjson argv "$argv_json" \
254
+ --argjson dur "$dur" \
255
+ --argjson rc "$rc" \
256
+ --argjson issues "$issues_arg" \
257
+ --argjson truncated "$truncated" \
258
+ '{file_path:$file,file_path_relative:$rel,tool_name:$tool,check_name:$name,check_kind:$kind,argv:$argv,duration_ms:$dur,exit_code:$rc,issue_count:$issues,output_excerpt:$output,output_truncated:$truncated,project_key:$pk}')
259
+ inspector_emit_event "inspector.check.failed" "$payload" || true
260
+ }
261
+
262
+ _inspector_emit_skipped() {
263
+ local name="$1" kind="$2" reason="$3"
264
+ local payload
265
+ payload=$(jq -n \
266
+ --arg file "$INSPECTOR_FILE" \
267
+ --arg rel "$INSPECTOR_FILE_RELATIVE" \
268
+ --arg tool "$INSPECTOR_TOOL_NAME" \
269
+ --arg name "$name" \
270
+ --arg kind "$kind" \
271
+ --arg reason "$reason" \
272
+ --arg pk "$INSPECTOR_PROJECT_KEY" \
273
+ '{file_path:$file,file_path_relative:$rel,tool_name:$tool,check_name:$name,check_kind:$kind,reason:$reason,project_key:$pk}')
274
+ inspector_emit_event "inspector.check.skipped" "$payload" || true
275
+ }
276
+
277
+ inspector_emit_whole_file_skipped() {
278
+ local reason="$1"
279
+ local payload
280
+ payload=$(jq -n \
281
+ --arg file "${INSPECTOR_FILE:-}" \
282
+ --arg rel "${INSPECTOR_FILE_RELATIVE:-}" \
283
+ --arg tool "${INSPECTOR_TOOL_NAME:-}" \
284
+ --arg reason "$reason" \
285
+ --arg pk "${INSPECTOR_PROJECT_KEY:-}" \
286
+ '{file_path:$file,file_path_relative:$rel,tool_name:$tool,reason:$reason,project_key:$pk}')
287
+ inspector_emit_event "inspector.check.skipped" "$payload" || true
288
+ }
289
+
290
+ _inspector_emit_run_completed() {
291
+ local ran="$1" passed="$2" failed="$3" skipped="$4" dur="$5"
292
+ local payload
293
+ payload=$(jq -n \
294
+ --arg file "$INSPECTOR_FILE" \
295
+ --arg rel "$INSPECTOR_FILE_RELATIVE" \
296
+ --arg tool "$INSPECTOR_TOOL_NAME" \
297
+ --arg pk "$INSPECTOR_PROJECT_KEY" \
298
+ --argjson ran "$ran" \
299
+ --argjson passed "$passed" \
300
+ --argjson failed "$failed" \
301
+ --argjson skipped "$skipped" \
302
+ --argjson dur "$dur" \
303
+ '{file_path:$file,file_path_relative:$rel,tool_name:$tool,checks_run:$ran,checks_passed:$passed,checks_failed:$failed,checks_skipped:$skipped,duration_ms:$dur,project_key:$pk}')
304
+ inspector_emit_event "inspector.run.completed" "$payload" || true
305
+ }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # inspector-ulid.sh — ULID generation for Inspector.
3
+ #
4
+ # 10-char Crockford Base32 timestamp + 16-char random component = 26 chars.
5
+ #
6
+ # Usage:
7
+ # id=$(inspector_ulid)
8
+
9
+ _INSPECTOR_ULID_ALPHABET="0123456789ABCDEFGHJKMNPQRSTVWXYZ"
10
+
11
+ _inspector_ulid_encode() {
12
+ local n="${1:-0}"
13
+ local len="${2:-10}"
14
+ local result=""
15
+ local i
16
+ for (( i = 0; i < len; i++ )); do
17
+ result="${_INSPECTOR_ULID_ALPHABET:$(( n & 31 )):1}${result}"
18
+ n=$(( n >> 5 ))
19
+ done
20
+ printf '%s' "$result"
21
+ }
22
+
23
+ inspector_ulid() {
24
+ local ts_ms
25
+ if date +%s%3N &>/dev/null && [[ "$(date +%s%3N)" =~ ^[0-9]{13}$ ]]; then
26
+ ts_ms=$(date +%s%3N)
27
+ else
28
+ ts_ms=$(python3 -c 'import time; print(int(time.time() * 1000))')
29
+ fi
30
+
31
+ local ts_encoded
32
+ ts_encoded=$(_inspector_ulid_encode "$ts_ms" 10)
33
+
34
+ local rand_hex
35
+ rand_hex=$(openssl rand -hex 10 2>/dev/null) \
36
+ || rand_hex=$(printf '%020x' $(( (RANDOM * RANDOM & 0xFFFFF) * 0x100000 + (RANDOM * RANDOM & 0xFFFFF) )))
37
+
38
+ local rand_hi rand_lo
39
+ rand_hi=$(( 16#${rand_hex:0:10} ))
40
+ rand_lo=$(( 16#${rand_hex:10:10} ))
41
+ local rand_encoded
42
+ rand_encoded="$(_inspector_ulid_encode "$rand_hi" 8)$(_inspector_ulid_encode "$rand_lo" 8)"
43
+
44
+ printf '%s%s' "$ts_encoded" "$rand_encoded"
45
+ }
@@ -67,7 +67,7 @@
67
67
  "changelog-path": "CHANGELOG.md",
68
68
  "release-type": "simple",
69
69
  "bump-minor-pre-major": true,
70
- "bump-patch-for-bump-minor-pre-major": false,
70
+ "bump-patch-for-minor-pre-major": false,
71
71
  "component": "cartographer",
72
72
  "draft": false,
73
73
  "prerelease": false,
@@ -254,6 +254,22 @@
254
254
  "jsonpath": "$.version"
255
255
  }
256
256
  ]
257
+ },
258
+ "plugins/inspector": {
259
+ "changelog-path": "CHANGELOG.md",
260
+ "release-type": "simple",
261
+ "bump-minor-pre-major": true,
262
+ "bump-patch-for-minor-pre-major": false,
263
+ "component": "inspector",
264
+ "draft": false,
265
+ "prerelease": false,
266
+ "extra-files": [
267
+ {
268
+ "type": "json",
269
+ "path": ".claude-plugin/plugin.json",
270
+ "jsonpath": "$.version"
271
+ }
272
+ ]
257
273
  }
258
274
  },
259
275
  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"