@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.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +2 -2
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +2 -0
- 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/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/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/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
|
@@ -92,21 +92,18 @@ _compass_matches_skip_glob() {
|
|
|
92
92
|
local globs_json="$2"
|
|
93
93
|
[[ -z "$file_path" || -z "$globs_json" ]] && return 1
|
|
94
94
|
|
|
95
|
-
#
|
|
96
|
-
local
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Use bash glob matching (extglob not needed for ** — use case).
|
|
102
|
-
# Convert ** to a catch-all for simple prefix/suffix matching.
|
|
103
|
-
local pattern="${glob//\*\*/DOUBLE_STAR}"
|
|
95
|
+
# Bash 3.2 (macOS) lacks mapfile, so stream via read-while.
|
|
96
|
+
local glob pattern
|
|
97
|
+
while IFS= read -r glob; do
|
|
98
|
+
[[ -z "$glob" ]] && continue
|
|
99
|
+
# Translate ** → .* and * → [^/]* for simple prefix/suffix matching.
|
|
100
|
+
pattern="${glob//\*\*/DOUBLE_STAR}"
|
|
104
101
|
pattern="${pattern//\*/[^/]*}"
|
|
105
102
|
pattern="${pattern//DOUBLE_STAR/.*}"
|
|
106
103
|
if [[ "$file_path" =~ $pattern ]]; then
|
|
107
104
|
return 0
|
|
108
105
|
fi
|
|
109
|
-
done
|
|
106
|
+
done < <(printf '%s' "$globs_json" | jq -r '.[]' 2>/dev/null)
|
|
110
107
|
return 1
|
|
111
108
|
}
|
|
112
109
|
|
|
@@ -241,12 +238,13 @@ Choose a path:
|
|
|
241
238
|
|
|
242
239
|
# -----------------------------------------------------------------------
|
|
243
240
|
# Main gate function.
|
|
244
|
-
# $1 — tool_name
|
|
245
|
-
# $2 — file_path
|
|
246
|
-
# $3 — operation
|
|
247
|
-
# $4 — context
|
|
241
|
+
# $1 — tool_name (Write | Edit | MultiEdit | Bash)
|
|
242
|
+
# $2 — file_path (may be empty for Bash)
|
|
243
|
+
# $3 — operation (write | edit | multi_edit | bash_write)
|
|
244
|
+
# $4 — context (context excerpt or bash command string)
|
|
248
245
|
# $5 — session_id
|
|
249
246
|
# $6 — cwd
|
|
247
|
+
# $7 — transcript_path (from hook JSON; may be empty)
|
|
250
248
|
# -----------------------------------------------------------------------
|
|
251
249
|
compass_run_gate() {
|
|
252
250
|
local tool_name="$1"
|
|
@@ -255,6 +253,7 @@ compass_run_gate() {
|
|
|
255
253
|
local context="$4"
|
|
256
254
|
local session_id="${5:-unknown}"
|
|
257
255
|
local cwd="${6:-}"
|
|
256
|
+
local transcript_path="${7:-}"
|
|
258
257
|
|
|
259
258
|
local _allow_exit=0
|
|
260
259
|
local _block_exit=0
|
|
@@ -348,14 +347,12 @@ compass_run_gate() {
|
|
|
348
347
|
_compass_increment_turn_count "$session_id" 2>/dev/null || true
|
|
349
348
|
|
|
350
349
|
# ---- Read prior assistant turn -----------------------------------
|
|
351
|
-
local prior_turn_chars_max
|
|
350
|
+
local prior_turn_chars_max
|
|
352
351
|
prior_turn_chars_max=$(compass_config_get '.compass.transcript.prior_turn_chars_max')
|
|
353
352
|
prior_turn_chars_max="${prior_turn_chars_max:-800}"
|
|
354
|
-
transcript_max_age=$(compass_config_get '.compass.transcript.transcript_max_age_seconds')
|
|
355
|
-
transcript_max_age="${transcript_max_age:-300}"
|
|
356
353
|
|
|
357
354
|
local prior_turn=""
|
|
358
|
-
prior_turn=$(compass_read_prior_turn "$
|
|
355
|
+
prior_turn=$(compass_read_prior_turn "$transcript_path" "$prior_turn_chars_max") \
|
|
359
356
|
|| prior_turn=""
|
|
360
357
|
|
|
361
358
|
# ---- Symbolic skip layer -----------------------------------------
|
|
@@ -42,13 +42,13 @@ _compass_strip_control_chars() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
# Replace all occurrences of a literal string with [STRIPPED].
|
|
45
|
+
# Uses bash native parameter expansion to avoid the sed delimiter trap —
|
|
46
|
+
# needles like </prior_assistant_turn> contain '/' which breaks sed s///.
|
|
45
47
|
_compass_strip_literal() {
|
|
46
48
|
local input="$1"
|
|
47
49
|
local needle="$2"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
escaped_needle=$(printf '%s' "$needle" | sed 's/[[\.*^$()+?{|]/\\&/g' 2>/dev/null) || escaped_needle="$needle"
|
|
51
|
-
printf '%s' "$input" | sed "s/${escaped_needle}/[STRIPPED]/g" 2>/dev/null
|
|
50
|
+
[[ -z "$needle" ]] && { printf '%s' "$input"; return; }
|
|
51
|
+
printf '%s' "${input//"$needle"/[STRIPPED]}"
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
# Truncate a string to at most max_chars UTF-8 characters.
|
|
@@ -1,135 +1,102 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Prior assistant turn reader for Compass.
|
|
3
3
|
#
|
|
4
|
-
# Resolves the most recent assistant turn from the session transcript
|
|
5
|
-
# the evaluator can operate on the pair {prior_assistant_turn, context}
|
|
6
|
-
# rather than context alone
|
|
7
|
-
# turns
|
|
4
|
+
# Resolves the most recent assistant turn from the session transcript JSONL
|
|
5
|
+
# so the evaluator can operate on the pair {prior_assistant_turn, context}
|
|
6
|
+
# rather than context alone. Avoids false positives on question-answer
|
|
7
|
+
# turns. See ADR-001 (plugins/compass/docs/adr/001-evaluate-prompts-in-context.md).
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# filtered by session_id and event_type:"session.prompt", most recent
|
|
14
|
-
# assistant-role entry
|
|
15
|
-
# 3. Empty string — degrades gracefully; evaluator runs on context alone
|
|
9
|
+
# Source: the `transcript_path` field from the hook JSON payload. This is
|
|
10
|
+
# the same field tribunal-stop-gate.sh reads (`jq -r '.transcript_path // ""'`).
|
|
11
|
+
# When `transcript_path` is absent or unreadable, this function returns the
|
|
12
|
+
# empty string and the evaluator runs on the current context alone.
|
|
16
13
|
#
|
|
17
14
|
# Exposes:
|
|
18
|
-
# compass_read_prior_turn <
|
|
19
|
-
# Echoes the prior assistant turn
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
[
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# Age check — skip entries older than max_age_seconds.
|
|
42
|
-
if [[ "$max_age_seconds" -gt 0 ]]; then
|
|
43
|
-
ts=$(printf '%s' "$line" | jq -r '.timestamp // empty' 2>/dev/null) || ts=""
|
|
44
|
-
if [[ -n "$ts" ]]; then
|
|
45
|
-
local entry_time
|
|
46
|
-
entry_time=$(date -d "$ts" +%s 2>/dev/null) \
|
|
47
|
-
|| entry_time=$(date -jf '%Y-%m-%dT%H:%M:%SZ' "$ts" +%s 2>/dev/null) \
|
|
48
|
-
|| entry_time=0
|
|
49
|
-
local age=$(( now - entry_time ))
|
|
50
|
-
[[ "$age" -gt "$max_age_seconds" ]] && continue
|
|
51
|
-
fi
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
content=$(printf '%s' "$line" | jq -r '.content // .text // empty' 2>/dev/null) || continue
|
|
55
|
-
[[ -n "$content" ]] && prior_turn="$content"
|
|
56
|
-
done < "$transcript_path"
|
|
57
|
-
|
|
58
|
-
[[ -n "$prior_turn" ]] || return 1
|
|
59
|
-
printf '%s' "$prior_turn"
|
|
15
|
+
# compass_read_prior_turn <transcript_path> <max_chars>
|
|
16
|
+
# Echoes the sanitized, truncated prior assistant turn, or empty string.
|
|
17
|
+
|
|
18
|
+
# Extract the text portion of a transcript line. The Claude Code session
|
|
19
|
+
# transcript stores assistant messages as `{"type":"assistant","message":{...}}`
|
|
20
|
+
# where `message.content` may be a string or an array of content blocks.
|
|
21
|
+
_compass_transcript_extract_text() {
|
|
22
|
+
local line="$1"
|
|
23
|
+
# Prefer message.content[*].text for the array-of-blocks shape; fall back
|
|
24
|
+
# to message.content when it is already a string. Final fallback: any
|
|
25
|
+
# top-level .content or .text field (covers legacy/alternate writers).
|
|
26
|
+
printf '%s' "$line" | jq -r '
|
|
27
|
+
if (.message? // null) != null then
|
|
28
|
+
if (.message.content | type) == "array" then
|
|
29
|
+
[.message.content[]? | select(type == "object") | (.text // "")]
|
|
30
|
+
| map(select(. != "")) | join("\n")
|
|
31
|
+
elif (.message.content | type) == "string" then
|
|
32
|
+
.message.content
|
|
33
|
+
else "" end
|
|
34
|
+
else
|
|
35
|
+
(.content // .text // "")
|
|
36
|
+
end
|
|
37
|
+
' 2>/dev/null
|
|
60
38
|
}
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
local
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
[[ -n "$session_id" && "$session_id" != "unknown" ]] || return 1
|
|
69
|
-
|
|
70
|
-
local now
|
|
71
|
-
now=$(date +%s 2>/dev/null) || now=0
|
|
72
|
-
|
|
73
|
-
local prior_turn=""
|
|
74
|
-
local line
|
|
75
|
-
while IFS= read -r line; do
|
|
76
|
-
[[ -z "$line" ]] && continue
|
|
77
|
-
|
|
78
|
-
local sid etype role
|
|
79
|
-
sid=$(printf '%s' "$line" | jq -r '.session_id // empty' 2>/dev/null) || continue
|
|
80
|
-
[[ "$sid" == "$session_id" ]] || continue
|
|
81
|
-
|
|
82
|
-
etype=$(printf '%s' "$line" | jq -r '.event_type // empty' 2>/dev/null) || continue
|
|
83
|
-
[[ "$etype" == "session.prompt" ]] || continue
|
|
84
|
-
|
|
85
|
-
role=$(printf '%s' "$line" | jq -r '.payload.role // empty' 2>/dev/null) || continue
|
|
86
|
-
[[ "$role" == "assistant" ]] || continue
|
|
40
|
+
# Return the role for a transcript line, falling back to .message.role.
|
|
41
|
+
_compass_transcript_role() {
|
|
42
|
+
local line="$1"
|
|
43
|
+
printf '%s' "$line" \
|
|
44
|
+
| jq -r '(.role // .message.role // .type // empty)' 2>/dev/null
|
|
45
|
+
}
|
|
87
46
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
47
|
+
# Walk a JSONL transcript file backwards to find the most recent assistant
|
|
48
|
+
# turn with non-empty text. Avoids loading the entire file into memory by
|
|
49
|
+
# streaming with `tac` when available; falls back to `tail -r` on BSD.
|
|
50
|
+
_compass_transcript_read_from_file() {
|
|
51
|
+
local path="$1"
|
|
52
|
+
[[ -f "$path" ]] || return 1
|
|
53
|
+
|
|
54
|
+
local reverser=""
|
|
55
|
+
if command -v tac >/dev/null 2>&1; then
|
|
56
|
+
reverser="tac"
|
|
57
|
+
elif command -v tail >/dev/null 2>&1 && tail -r </dev/null >/dev/null 2>&1; then
|
|
58
|
+
reverser="tail -r"
|
|
59
|
+
fi
|
|
99
60
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
61
|
+
local line role content
|
|
62
|
+
if [[ -n "$reverser" ]]; then
|
|
63
|
+
while IFS= read -r line; do
|
|
64
|
+
[[ -z "$line" ]] && continue
|
|
65
|
+
role=$(_compass_transcript_role "$line") || continue
|
|
66
|
+
[[ "$role" == "assistant" ]] || continue
|
|
67
|
+
content=$(_compass_transcript_extract_text "$line") || continue
|
|
68
|
+
[[ -n "$content" ]] && { printf '%s' "$content"; return 0; }
|
|
69
|
+
done < <(eval "$reverser" "\"$path\"" 2>/dev/null)
|
|
70
|
+
else
|
|
71
|
+
# Final fallback: forward scan, keep the last match.
|
|
72
|
+
local found=""
|
|
73
|
+
while IFS= read -r line; do
|
|
74
|
+
[[ -z "$line" ]] && continue
|
|
75
|
+
role=$(_compass_transcript_role "$line") || continue
|
|
76
|
+
[[ "$role" == "assistant" ]] || continue
|
|
77
|
+
content=$(_compass_transcript_extract_text "$line") || continue
|
|
78
|
+
[[ -n "$content" ]] && found="$content"
|
|
79
|
+
done < "$path"
|
|
80
|
+
[[ -n "$found" ]] && { printf '%s' "$found"; return 0; }
|
|
81
|
+
fi
|
|
104
82
|
|
|
105
|
-
|
|
106
|
-
printf '%s' "$prior_turn"
|
|
83
|
+
return 1
|
|
107
84
|
}
|
|
108
85
|
|
|
109
86
|
# Read the prior assistant turn.
|
|
110
|
-
# $1 —
|
|
87
|
+
# $1 — transcript_path (from hook JSON payload; may be empty)
|
|
111
88
|
# $2 — max_chars (from config: transcript.prior_turn_chars_max)
|
|
112
|
-
#
|
|
113
|
-
# Echoes the sanitized, truncated prior assistant turn, or empty string.
|
|
89
|
+
# Echoes the sanitized, truncated prior assistant turn, or the empty string.
|
|
114
90
|
compass_read_prior_turn() {
|
|
115
|
-
local
|
|
91
|
+
local transcript_path="${1:-}"
|
|
116
92
|
local max_chars="${2:-800}"
|
|
117
|
-
local max_age_seconds="${3:-300}"
|
|
118
93
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Source 1: CLAUDE_TRANSCRIPT_PATH
|
|
122
|
-
if [[ -n "${CLAUDE_TRANSCRIPT_PATH:-}" ]]; then
|
|
123
|
-
raw=$(_compass_transcript_from_path "$CLAUDE_TRANSCRIPT_PATH" "$session_id" "$max_age_seconds") || raw=""
|
|
124
|
-
fi
|
|
125
|
-
|
|
126
|
-
# Source 2: Onlooker event log
|
|
127
|
-
if [[ -z "$raw" ]]; then
|
|
128
|
-
raw=$(_compass_transcript_from_event_log "$session_id" "$max_age_seconds") || raw=""
|
|
129
|
-
fi
|
|
94
|
+
[[ -z "$transcript_path" ]] && return 0
|
|
130
95
|
|
|
96
|
+
local raw=""
|
|
97
|
+
raw=$(_compass_transcript_read_from_file "$transcript_path") || raw=""
|
|
131
98
|
[[ -z "$raw" ]] && return 0
|
|
132
99
|
|
|
133
|
-
#
|
|
100
|
+
# compass-sanitizer.sh is sourced by the caller (hook script).
|
|
134
101
|
compass_sanitize "$raw" "$max_chars"
|
|
135
102
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inspector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Per-edit lint and typecheck gate. Runs the project's configured checks on just the touched file after every Write / Edit / MultiEdit, so the agent sees its own type errors before claiming success. Cheaper than proctor (which runs project-wide verification at Stop); complements assayer (which catches the agent lying about claims). Builds on the Onlooker ecosystem plugin.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Onlooker Community",
|
|
7
|
+
"url": "https://onlooker.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://onlooker.dev",
|
|
10
|
+
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"skills": [],
|
|
13
|
+
"agents": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Inspector
|
|
2
|
+
|
|
3
|
+
Per-edit lint and typecheck gate for the Onlooker ecosystem — runs the project's
|
|
4
|
+
configured checks on **just the touched file** after every `Write`, `Edit`, and
|
|
5
|
+
`MultiEdit`, so the agent sees its own lint and type errors before it claims
|
|
6
|
+
success.
|
|
7
|
+
|
|
8
|
+
Inspector is a sibling plugin to [`ecosystem`](../../) and assumes the Onlooker
|
|
9
|
+
observability substrate (`~/.onlooker/`) is present.
|
|
10
|
+
|
|
11
|
+
## Why it exists
|
|
12
|
+
|
|
13
|
+
The ecosystem already has plugins that judge agent output after the fact and
|
|
14
|
+
plugins that gate ambiguous writes before they happen. What it didn't have until
|
|
15
|
+
now is a fast feedback loop that runs after every edit and tells the agent
|
|
16
|
+
*whether the code it just wrote actually compiles*. Inspector is that loop.
|
|
17
|
+
|
|
18
|
+
- **Not [proctor]** (planned) — proctor runs the project's full verification
|
|
19
|
+
command at `Stop`. Inspector runs only on touched files, only on `PostToolUse`.
|
|
20
|
+
Cheaper, fires far more often, narrower scope.
|
|
21
|
+
- **Not [assayer]** — assayer parses the agent's final message for testable
|
|
22
|
+
claims and cross-checks them against actual exit codes in the transcript.
|
|
23
|
+
Assayer catches the agent *lying* about claims. Inspector ensures the agent
|
|
24
|
+
has *accurate ground truth* to make claims from. They compose: inspector
|
|
25
|
+
emits real pass/fail signals; assayer can later confirm the agent's claims
|
|
26
|
+
line up with those signals.
|
|
27
|
+
- **Not a build system** — inspector runs the configured command, captures the
|
|
28
|
+
result, emits an event, exits. No cross-file caching, no dependency graphs.
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
| Hook | Matcher | What Inspector does |
|
|
33
|
+
|------|---------|---------------------|
|
|
34
|
+
| `PostToolUse` | `Edit`, `Write`, `MultiEdit` | Resolves the touched file from `tool_input.file_path`, looks up the configured checks for the file's extension, runs each check with a per-check timeout, and emits `inspector.check.passed` / `.failed` / `.skipped` plus a single `inspector.run.completed` summary. Bounded by `total_timeout_seconds`. Always exits 0 — inspector is advisory and never blocks the tool call. |
|
|
35
|
+
|
|
36
|
+
The hook's stdout (the additional-context channel for `PostToolUse` hooks) is
|
|
37
|
+
the agent-facing summary. By default it's quiet on clean runs:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
inspector: src/cart.ts
|
|
41
|
+
✗ biome (3 issues, exit 1)
|
|
42
|
+
src/cart.ts:42:5 — Unused variable 'subtotal'
|
|
43
|
+
src/cart.ts:51:3 — Missing return type annotation
|
|
44
|
+
src/cart.ts:64:9 — Unreachable code
|
|
45
|
+
✗ tsc (1 issue(s), exit 2)
|
|
46
|
+
src/cart.ts:42:5 — Type 'string | undefined' is not assignable to 'string'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Set `inspector.show_clean_runs: true` to surface the file header on passing
|
|
50
|
+
checks too. The agent sees this on its next turn.
|
|
51
|
+
|
|
52
|
+
## Activation
|
|
53
|
+
|
|
54
|
+
Inspector ships disabled. Opt in per project (or globally) by adding the
|
|
55
|
+
`inspector` block to `.claude/settings.json`:
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"inspector": {
|
|
60
|
+
"enabled": true,
|
|
61
|
+
"checks": {
|
|
62
|
+
".ts": [{ "name": "biome", "kind": "lint", "argv": ["biome", "check", "${file}"] },
|
|
63
|
+
{ "name": "tsc", "kind": "typecheck", "argv": ["tsc", "--noEmit"] }],
|
|
64
|
+
".tsx": [{ "name": "biome", "kind": "lint", "argv": ["biome", "check", "${file}"] },
|
|
65
|
+
{ "name": "tsc", "kind": "typecheck", "argv": ["tsc", "--noEmit"] }],
|
|
66
|
+
".py": [{ "name": "ruff", "kind": "lint", "argv": ["ruff", "check", "${file}"] }],
|
|
67
|
+
".sh": [{ "name": "shellcheck", "kind": "lint", "argv": ["shellcheck", "${file}"] }]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Each check is an `{ name, kind, argv }` object. `kind` is one of `lint` or
|
|
74
|
+
`typecheck` (used for downstream grouping). `argv` is the literal argv array;
|
|
75
|
+
the following placeholders are expanded before exec:
|
|
76
|
+
|
|
77
|
+
| Placeholder | Resolves to |
|
|
78
|
+
|-------------------|--------------------------------------------|
|
|
79
|
+
| `${file}` | absolute path to the touched file |
|
|
80
|
+
| `${file_relative}`| path relative to the repo root |
|
|
81
|
+
| `${repo_root}` | the repo's `git rev-parse --show-toplevel` |
|
|
82
|
+
|
|
83
|
+
A bare argv array (`["shellcheck", "${file}"]`) is also accepted as a shorthand
|
|
84
|
+
— inspector treats the first entry as the check name and the kind as `lint`.
|
|
85
|
+
|
|
86
|
+
## Config
|
|
87
|
+
|
|
88
|
+
| Field | Default | Meaning |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `enabled` | `false` | Master switch. |
|
|
91
|
+
| `timeout_seconds_per_check` | `10` | Wall-clock cap per check. Exceeded → `inspector.check.skipped` with `reason: "timeout"`. |
|
|
92
|
+
| `total_timeout_seconds` | `30` | Wall-clock cap for the whole run. Remaining checks emit `.skipped` with `reason: "total_budget_exhausted"`. |
|
|
93
|
+
| `output_excerpt_max_bytes` | `4096` | Cap on captured output, both in the event and shown to the agent. Excess is replaced with `…[truncated]`. |
|
|
94
|
+
| `show_clean_runs` | `false` | If `true`, the agent-facing summary includes passing checks too. Off by default to keep token usage low. |
|
|
95
|
+
| `exclude_paths` | `["node_modules", ".git", "vendor", ".venv", "dist", ".next", ".nuxt", "build", "__pycache__", "target", "coverage"]` | Containment match against the file's path relative to the repo root. A match emits `inspector.check.skipped` with `reason: "excluded_path"` and runs no checks. |
|
|
96
|
+
| `checks` | `{}` | Map of file extension (with leading dot) to an array of check definitions. Empty by default — opt in per project. |
|
|
97
|
+
|
|
98
|
+
Config precedence: plugin defaults < `~/.claude/settings.json` (`.inspector`) <
|
|
99
|
+
`<repo>/.claude/settings.json` (`.inspector`). Each layer fully replaces the
|
|
100
|
+
`checks` array for a given extension; per-entry deep-merge is intentionally not
|
|
101
|
+
supported because the override behavior would be unpredictable.
|
|
102
|
+
|
|
103
|
+
## Events
|
|
104
|
+
|
|
105
|
+
All events are registered in `@onlooker-community/schema`.
|
|
106
|
+
|
|
107
|
+
| Event | When | Notable payload fields |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `inspector.check.passed` | A check returned exit 0 | `file_path`, `tool_name`, `check_name`, `check_kind`, `argv`, `duration_ms` |
|
|
110
|
+
| `inspector.check.failed` | A check returned non-zero | `exit_code`, `issue_count` (best-effort, may be `null`), `output_excerpt`, `output_truncated` |
|
|
111
|
+
| `inspector.check.skipped` | A check or whole file was not run | `reason`: one of `disabled`, `excluded_path`, `no_extension_match`, `not_in_repo`, `tool_missing`, `timeout`, `total_budget_exhausted` |
|
|
112
|
+
| `inspector.run.completed` | Once per hook fire after all checks | `checks_run`, `checks_passed`, `checks_failed`, `checks_skipped`, `duration_ms` |
|
|
113
|
+
|
|
114
|
+
Downstream consumers that just want "did this edit produce broken code?" read
|
|
115
|
+
`inspector.run.completed` and check `checks_failed > 0`. Consumers that want
|
|
116
|
+
per-tool detail subscribe to `inspector.check.*`.
|
|
117
|
+
|
|
118
|
+
## Whole-project checks (tsc, mypy, …)
|
|
119
|
+
|
|
120
|
+
TypeScript's typecheck is project-scoped: `tsc --noEmit` checks every file, not
|
|
121
|
+
just the touched one. There is no meaningful "tsc on one file." The supported
|
|
122
|
+
pattern is to run the full project tsc and rely on its incremental cache
|
|
123
|
+
(`tsBuildInfoFile`) to keep latency down. Same applies to mypy, cargo check,
|
|
124
|
+
and golangci-lint.
|
|
125
|
+
|
|
126
|
+
The downside is that `tsc --noEmit` reports errors in *every* file. Inspector's
|
|
127
|
+
v1 surfaces all of them to the agent. A follow-up will add an opt-in filter
|
|
128
|
+
that shows only errors mentioning the touched file plus a collateral-error
|
|
129
|
+
count.
|
|
130
|
+
|
|
131
|
+
## Failure modes
|
|
132
|
+
|
|
133
|
+
Inspector is advisory — it never blocks the tool call. Specifically:
|
|
134
|
+
|
|
135
|
+
- Missing tool on PATH → `.skipped` event, no agent-facing output
|
|
136
|
+
- Timeout → `.skipped` event, one-line note to the agent
|
|
137
|
+
- Hook script error → exit 0 with no event (last-resort path)
|
|
138
|
+
- Schema validation error → logged to stderr, hook continues
|
|
139
|
+
|
|
140
|
+
## Compatibility
|
|
141
|
+
|
|
142
|
+
- bash 3.2+ (the macOS system bash is supported; no `mapfile` / `readarray` /
|
|
143
|
+
associative arrays)
|
|
144
|
+
- `jq` is required (already a hard requirement for the ecosystem substrate)
|
|
145
|
+
- `timeout` from coreutils is used when present; falls back to no-timeout mode
|
|
146
|
+
when absent (and emits a warning to the inspector hook log)
|
|
147
|
+
|
|
148
|
+
## Design
|
|
149
|
+
|
|
150
|
+
See [`docs/design.md`](docs/design.md) for the full design record, including
|
|
151
|
+
the rationale for project-wide check semantics, output filtering, and open
|
|
152
|
+
questions.
|
|
153
|
+
|
|
154
|
+
[proctor]: https://github.com/onlooker-community/ecosystem#planned
|
|
155
|
+
[assayer]: ../assayer
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugin_name": "inspector",
|
|
3
|
+
"storage_path": "~/.onlooker",
|
|
4
|
+
"inspector": {
|
|
5
|
+
"enabled": false,
|
|
6
|
+
"timeout_seconds_per_check": 10,
|
|
7
|
+
"total_timeout_seconds": 30,
|
|
8
|
+
"output_excerpt_max_bytes": 4096,
|
|
9
|
+
"show_clean_runs": false,
|
|
10
|
+
"exclude_paths": [
|
|
11
|
+
"node_modules",
|
|
12
|
+
".git",
|
|
13
|
+
"vendor",
|
|
14
|
+
".venv",
|
|
15
|
+
"dist",
|
|
16
|
+
".next",
|
|
17
|
+
".nuxt",
|
|
18
|
+
"build",
|
|
19
|
+
"__pycache__",
|
|
20
|
+
"target",
|
|
21
|
+
"coverage"
|
|
22
|
+
],
|
|
23
|
+
"checks": {}
|
|
24
|
+
}
|
|
25
|
+
}
|