@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.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +4 -3
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +2 -0
- package/README.md +114 -13
- package/docs/plugin-catalog.md +125 -0
- package/package.json +3 -3
- package/plugins/compass/.claude-plugin/plugin.json +1 -1
- package/plugins/compass/CHANGELOG.md +7 -0
- package/plugins/compass/README.md +1 -3
- package/plugins/compass/config.json +1 -2
- package/plugins/compass/docs/design.md +1 -2
- package/plugins/compass/scripts/hooks/compass-bash-gate.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-record-write.sh +5 -0
- package/plugins/compass/scripts/hooks/compass-session-start.sh +0 -8
- package/plugins/compass/scripts/lib/compass-evaluator.sh +58 -98
- package/plugins/compass/scripts/lib/compass-gate.sh +15 -18
- package/plugins/compass/scripts/lib/compass-sanitizer.sh +4 -4
- package/plugins/compass/scripts/lib/compass-transcript.sh +79 -112
- package/plugins/inspector/.claude-plugin/plugin.json +14 -0
- package/plugins/inspector/CHANGELOG.md +8 -0
- package/plugins/inspector/README.md +155 -0
- package/plugins/inspector/config.json +25 -0
- package/plugins/inspector/docs/design.md +286 -0
- package/plugins/inspector/hooks/hooks.json +33 -0
- package/plugins/inspector/scripts/hooks/inspector-post-write.sh +124 -0
- package/plugins/inspector/scripts/lib/inspector-config.sh +108 -0
- package/plugins/inspector/scripts/lib/inspector-events.sh +82 -0
- package/plugins/inspector/scripts/lib/inspector-project-key.sh +55 -0
- package/plugins/inspector/scripts/lib/inspector-run.sh +305 -0
- package/plugins/inspector/scripts/lib/inspector-ulid.sh +45 -0
- package/release-please-config.json +17 -1
- package/test/bats/archivist-project-key.bats +79 -0
- package/test/bats/archivist-storage.bats +79 -0
- package/test/bats/compact-tracker.bats +125 -0
- package/test/bats/compass-config.bats +65 -0
- package/test/bats/compass-gate.bats +129 -0
- package/test/bats/compass-sanitizer.bats +69 -0
- package/test/bats/compass-symbolic-skip.bats +88 -0
- package/test/bats/compass-transcript.bats +80 -0
- package/test/bats/inspector-config.bats +118 -0
- package/test/bats/inspector-events.bats +156 -0
- package/test/bats/inspector-post-write-hook.bats +164 -0
- package/test/bats/inspector-project-key.bats +68 -0
- package/test/bats/inspector-ulid.bats +34 -0
- package/test/bats/librarian-session-end.bats +8 -1
- package/test/bats/onlooker-schema.bats +111 -0
- package/test/bats/prompt-rules.bats +98 -0
- package/test/bats/session-tracker.bats +260 -0
- package/test/bats/skill-usage-tracker.bats +63 -0
- package/test/bats/task-tracker.bats +102 -0
- package/test/bats/turn-tracker.bats +180 -0
- package/test/bats/validate-path.bats +125 -0
- 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-
|
|
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"
|