@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,286 @@
1
+ # Inspector — Design
2
+
3
+ **Layer:** verification / execution
4
+ **Hook surface:** `PostToolUse` for `Write`, `Edit`, `MultiEdit`
5
+ **Status:** initial design — first implementation under this doc
6
+
7
+ Inspector is the per-edit lint and typecheck gate. Every time the agent writes
8
+ to a file, inspector runs the project's configured checks on **just that file**
9
+ and surfaces the result back to the agent before its next turn. The agent sees
10
+ its own type errors immediately and self-corrects, instead of claiming success
11
+ on broken code and getting caught later by [assayer] or a human reviewer.
12
+
13
+ ## What inspector is — and isn't
14
+
15
+ - **Inspector is not proctor.** Proctor (planned) runs the project's full
16
+ verification command (`npm test`, `mise run check`, …) at `Stop`. Inspector
17
+ runs only on touched files, only on `PostToolUse`. Cheaper, fires far more
18
+ often, narrower scope.
19
+ - **Inspector is not assayer.** Assayer (shipped) parses the agent's final
20
+ message for testable claims ("the build passes", "I ran the tests") and
21
+ cross-checks them against actual exit codes in the transcript. Assayer catches
22
+ the agent *lying* about claims. Inspector ensures the agent has *accurate
23
+ ground truth* to make claims from. They compose: inspector emits real
24
+ pass/fail signals; assayer can later confirm the agent's claims line up with
25
+ those signals.
26
+ - **Inspector is not a build system.** It does not chain dependencies, cache
27
+ intermediate results, or share state across invocations beyond timeouts. It
28
+ runs the configured check, captures the result, emits an event, exits. If
29
+ configuration says "run tsc on a single file," it does that — even though
30
+ tsc's per-file mode loses project context. Choosing the right command for a
31
+ given language is the user's job; inspector executes what it is told.
32
+
33
+ ## Hook flow
34
+
35
+ ```
36
+ PostToolUse(Write|Edit|MultiEdit)
37
+ → inspector-post-write.sh
38
+ → resolve touched file from tool_input.file_path
39
+ → load merged config (plugin defaults < home < repo)
40
+ → if disabled OR excluded path OR no extension match → emit .skipped, exit 0
41
+ → for each configured check matching the extension:
42
+ → expand ${file}, ${file_relative}, ${repo_root} in the command
43
+ → run with per-check timeout
44
+ → emit inspector.check.passed / .failed / .skipped
45
+ → exit 0 (never blocks the tool call)
46
+ ```
47
+
48
+ The hook always exits 0. Inspector is advisory — it never blocks the agent's
49
+ write. It surfaces what it found in the additional-context channel of the
50
+ PostToolUse hook reply so the agent sees the result on its next turn.
51
+
52
+ ## Configuration
53
+
54
+ The minimum useful configuration is a map from file extension to a list of
55
+ commands to run. Each command is an argv array; `${file}` substitutes the
56
+ canonical absolute path to the touched file.
57
+
58
+ ```jsonc
59
+ {
60
+ "inspector": {
61
+ "enabled": false,
62
+ "timeout_seconds_per_check": 10,
63
+ "total_timeout_seconds": 30,
64
+ "exclude_paths": ["node_modules", ".git", "vendor", "dist", "build",
65
+ ".next", "__pycache__", ".venv"],
66
+ "checks": {
67
+ ".ts": [{ "name": "biome", "argv": ["biome", "check", "${file}"] },
68
+ { "name": "tsc", "argv": ["tsc", "--noEmit"] }],
69
+ ".tsx": [{ "name": "biome", "argv": ["biome", "check", "${file}"] },
70
+ { "name": "tsc", "argv": ["tsc", "--noEmit"] }],
71
+ ".py": [{ "name": "ruff", "argv": ["ruff", "check", "${file}"] }],
72
+ ".sh": [{ "name": "shellcheck", "argv": ["shellcheck", "${file}"] }]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Merging follows the standard ecosystem precedence (plugin defaults < home <
79
+ repo). Each layer can fully replace `checks` for a given extension by setting
80
+ the array; deep-merge of individual entries within an extension is intentionally
81
+ not supported — it makes the override behavior unpredictable.
82
+
83
+ `config.json` ships with `enabled: false` to match the rest of the ecosystem.
84
+ The first PR that ships inspector also adds an opt-in path in the README.
85
+
86
+ ## Path handling
87
+
88
+ - `file_path` is resolved via `realpath` where available, falling back to
89
+ `readlink -f`, falling back to the raw input.
90
+ - The file must be inside the current repo root (`git rev-parse --show-toplevel`
91
+ from `cwd`). Out-of-tree writes are skipped — inspector is per-project.
92
+ - `exclude_paths` is matched against the file's path relative to the repo root.
93
+ Match semantics are *containment* (`vendor/foo.ts` matches `vendor`), not
94
+ glob. Compass uses globs; inspector uses containment because the use case is
95
+ "skip this whole directory tree."
96
+
97
+ ## Per-check execution
98
+
99
+ Each check runs in the repo root (`cwd`) with the following environment:
100
+
101
+ - inherited PATH plus any `mise`-shimmed bins (already set up at session start)
102
+ - `INSPECTOR_FILE` — absolute path to the touched file
103
+ - `INSPECTOR_FILE_RELATIVE` — relative to repo root
104
+ - `INSPECTOR_REPO_ROOT` — the repo root
105
+ - `INSPECTOR_PROJECT_KEY` — the project key
106
+
107
+ `timeout_seconds_per_check` is enforced via the `timeout` command (or a
108
+ fallback bash trap on systems that lack it). On timeout, inspector emits
109
+ `inspector.check.skipped` with `reason: "timeout"`.
110
+
111
+ If the command's first argv entry is not on PATH, inspector emits
112
+ `inspector.check.skipped` with `reason: "tool_missing"`. This is the dominant
113
+ "new project, lint not installed yet" case and must be quiet — no error in the
114
+ hook output, just the skipped event in the log.
115
+
116
+ ## What the agent sees
117
+
118
+ The hook's stdout (the additional-context channel for PostToolUse hooks)
119
+ contains a compact, one-line-per-finding summary:
120
+
121
+ ```
122
+ inspector: src/cart.ts
123
+ ✗ biome (3 issues)
124
+ src/cart.ts:42:5 — Unused variable 'subtotal'
125
+ src/cart.ts:51:3 — Missing return type annotation
126
+ src/cart.ts:64:9 — Unreachable code
127
+ ✗ tsc (1 error)
128
+ src/cart.ts:42:5 — Type 'string | undefined' is not assignable to 'string'
129
+ ```
130
+
131
+ On clean runs, inspector either emits nothing to stdout (the silent case) or a
132
+ single confirmation line, controlled by `inspector.show_clean_runs`
133
+ (default `false`). Default silence avoids token spam on every edit.
134
+
135
+ Output capture per check is bounded by `inspector.output_excerpt_max_bytes`
136
+ (default 4096) — beyond that, inspector truncates with a `…[truncated]` marker
137
+ both in the agent-facing output and in the event payload.
138
+
139
+ ## tsc and other whole-project checks
140
+
141
+ TypeScript's typecheck is project-scoped: `tsc --noEmit` checks every file in
142
+ the project, not just the touched one. Inspector cannot meaningfully run "tsc
143
+ on a single file" — `tsc --noEmit src/foo.ts` runs in a degraded mode that
144
+ loses project context (imports, references, lib types).
145
+
146
+ The right behavior is to run `tsc --noEmit -p tsconfig.json` (full project)
147
+ and filter the output to errors that mention the touched file. The first
148
+ implementation runs the full project tsc and post-filters. This is more
149
+ expensive than the lint case but cached by tsc's incremental compilation
150
+ (`tsBuildInfoFile`). Users who care about latency can disable the tsc check
151
+ per-project.
152
+
153
+ Other languages with similar whole-project semantics (mypy, cargo check,
154
+ golangci-lint) follow the same pattern: run the project-wide command, filter
155
+ results to the touched file. Users opt into these in `config.json` because the
156
+ cost is higher than per-file lint.
157
+
158
+ ## Events
159
+
160
+ All four events are registered in `@onlooker-community/schema` under
161
+ `plugins-verification.json`.
162
+
163
+ ### `inspector.check.passed`
164
+
165
+ Emitted once per check that passed cleanly.
166
+
167
+ ```jsonc
168
+ {
169
+ "file_path": "/abs/path/to/src/cart.ts",
170
+ "file_path_relative": "src/cart.ts",
171
+ "tool_name": "Edit", // Write | Edit | MultiEdit
172
+ "check_name": "biome",
173
+ "check_kind": "lint", // lint | typecheck
174
+ "argv": ["biome", "check", "/abs/path/to/src/cart.ts"],
175
+ "duration_ms": 124
176
+ }
177
+ ```
178
+
179
+ ### `inspector.check.failed`
180
+
181
+ Emitted once per check that returned a non-zero exit code.
182
+
183
+ ```jsonc
184
+ {
185
+ "file_path": "/abs/path/to/src/cart.ts",
186
+ "file_path_relative": "src/cart.ts",
187
+ "tool_name": "Edit",
188
+ "check_name": "tsc",
189
+ "check_kind": "typecheck",
190
+ "argv": ["tsc", "--noEmit"],
191
+ "duration_ms": 980,
192
+ "exit_code": 2,
193
+ "issue_count": 3,
194
+ "output_excerpt": "src/cart.ts:42:5 — Type 'string | undefined' is not assignable to 'string'\n…"
195
+ }
196
+ ```
197
+
198
+ `issue_count` is best-effort: inspector parses common output formats (one
199
+ issue per non-empty line, ignoring obvious headers/footers) where possible
200
+ and falls back to `null` when the format is unknown.
201
+
202
+ ### `inspector.check.skipped`
203
+
204
+ Emitted when a check did not run.
205
+
206
+ ```jsonc
207
+ {
208
+ "file_path": "/abs/path/to/src/cart.ts",
209
+ "file_path_relative": "src/cart.ts",
210
+ "tool_name": "Edit",
211
+ "check_name": "tsc", // optional — absent for whole-file skips
212
+ "reason": "tool_missing" // tool_missing | disabled | excluded_path
213
+ // | no_extension_match | timeout
214
+ // | not_in_repo | total_budget_exhausted
215
+ }
216
+ ```
217
+
218
+ ### `inspector.run.completed`
219
+
220
+ Emitted once per hook invocation, after all per-check events for that file.
221
+
222
+ ```jsonc
223
+ {
224
+ "file_path": "/abs/path/to/src/cart.ts",
225
+ "file_path_relative": "src/cart.ts",
226
+ "tool_name": "Edit",
227
+ "checks_run": 2,
228
+ "checks_passed": 0,
229
+ "checks_failed": 2,
230
+ "checks_skipped": 0,
231
+ "duration_ms": 1104
232
+ }
233
+ ```
234
+
235
+ Downstream consumers that just want "did this edit produce broken code?" read
236
+ `inspector.run.completed` and check `checks_failed > 0`. Consumers that want
237
+ per-tool detail subscribe to `inspector.check.*`.
238
+
239
+ ## Project-key derivation
240
+
241
+ Same algorithm as the rest of the ecosystem:
242
+
243
+ ```
244
+ SHA256("remote:" + git remote get-url origin) → first 12 hex chars
245
+ → fall back to SHA256("root:" + git rev-parse --show-toplevel)[:12]
246
+ → fall back to SHA256("cwd:" + pwd)[:12]
247
+ ```
248
+
249
+ Implemented in `plugins/inspector/scripts/lib/inspector-project-key.sh`,
250
+ mirroring the equivalent helper in cartographer.
251
+
252
+ ## Failure modes and fail-soft behavior
253
+
254
+ Inspector is advisory. It never blocks the tool call. Specifically:
255
+
256
+ - Missing tool on PATH → `.skipped` event, no agent-facing output
257
+ - Timeout → `.skipped` event, agent sees `inspector: src/foo.ts — biome timed out`
258
+ - Hook script error → exit 0 with no event (last-resort; logged to
259
+ `~/.onlooker/inspector/<project>/hook.log`)
260
+ - Schema validation error → logged to stderr, hook continues
261
+ - Concurrent invocation → no lock; each check runs independently. tsc's own
262
+ incremental cache handles concurrent invocations safely
263
+
264
+ The "exit 0 with no event" path is the only case where inspector goes silent.
265
+ This is deliberate: inspector is a noticeably new behavior surface; bugs in the
266
+ hook script must not block writes.
267
+
268
+ ## Open questions
269
+
270
+ These are deferred to a follow-up ADR after first real-world use.
271
+
272
+ 1. **Output filtering for whole-project checks.** Filtering tsc/mypy output to
273
+ the touched file is necessary for "only what you broke just now" UX. But
274
+ compile errors in unrelated files often *are* caused by this edit (a removed
275
+ export breaks five importers). Showing only the touched-file errors hides
276
+ real damage; showing all errors floods the channel. Tentative: show only
277
+ touched-file errors by default; add `inspector.show_collateral_errors`
278
+ config knob if requests come in.
279
+ 2. **Caching.** Repeated edits to the same file within seconds will re-run all
280
+ checks. Worth caching at the level of "skip identical file content"? Not
281
+ for v1.
282
+ 3. **Parallel checks.** Checks for the same file run sequentially today. Worth
283
+ parallelizing? Not for v1 — most lints finish in <500ms and bash-level
284
+ parallelism with timeouts is fiddly.
285
+
286
+ [assayer]: ../../assayer/docs/design.md
@@ -0,0 +1,33 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/inspector-post-write.sh"
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "matcher": "Edit",
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/inspector-post-write.sh"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "matcher": "MultiEdit",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/inspector-post-write.sh"
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
33
+ }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bash
2
+ # inspector-post-write.sh — PostToolUse hook for Write / Edit / MultiEdit.
3
+ #
4
+ # Runs the project's configured lint and typecheck commands on just the
5
+ # touched file. Emits inspector.check.* and inspector.run.completed events.
6
+ # Surfaces a compact summary on stdout for the agent's next turn.
7
+ # Always exits 0 — inspector is advisory.
8
+
9
+ set -uo pipefail
10
+
11
+ # Recursion guard — prevents inspector from re-triggering itself if a check
12
+ # command happens to write to a watched file via its own tooling.
13
+ [[ "${INSPECTOR_NESTED:-0}" == "1" ]] && exit 0
14
+ export INSPECTOR_NESTED=1
15
+
16
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
17
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
18
+
19
+ source "$PLUGIN_ROOT/scripts/lib/inspector-config.sh"
20
+ source "$PLUGIN_ROOT/scripts/lib/inspector-project-key.sh"
21
+ source "$PLUGIN_ROOT/scripts/lib/inspector-events.sh"
22
+ source "$PLUGIN_ROOT/scripts/lib/inspector-run.sh"
23
+
24
+ HOOK_INPUT=$(cat)
25
+ CWD=$(printf '%s' "$HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null)
26
+ _HOOK_SESSION_ID=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // empty' 2>/dev/null)
27
+ TOOL_NAME=$(printf '%s' "$HOOK_INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
28
+ export _HOOK_SESSION_ID
29
+
30
+ # Bail on missing input — never block the tool call.
31
+ [[ -z "$CWD" ]] && exit 0
32
+ case "$TOOL_NAME" in
33
+ Write|Edit|MultiEdit) ;;
34
+ *) exit 0 ;;
35
+ esac
36
+ export INSPECTOR_TOOL_NAME="$TOOL_NAME"
37
+
38
+ # Resolve touched file from tool input.
39
+ TOOL_TARGET=$(printf '%s' "$HOOK_INPUT" \
40
+ | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
41
+ [[ -z "$TOOL_TARGET" ]] && exit 0
42
+
43
+ # Canonicalize.
44
+ if command -v realpath &>/dev/null; then
45
+ CANONICAL=$(realpath "$TOOL_TARGET" 2>/dev/null) || CANONICAL="$TOOL_TARGET"
46
+ elif command -v readlink &>/dev/null; then
47
+ CANONICAL=$(readlink -f "$TOOL_TARGET" 2>/dev/null) || CANONICAL="$TOOL_TARGET"
48
+ else
49
+ CANONICAL="$TOOL_TARGET"
50
+ fi
51
+ export INSPECTOR_FILE="$CANONICAL"
52
+
53
+ REPO_ROOT=$(inspector_project_repo_root "$CWD")
54
+ export INSPECTOR_REPO_ROOT="$REPO_ROOT"
55
+
56
+ # Project-key derivation — always succeeds (falls back to cwd hash).
57
+ PROJECT_KEY=$(inspector_project_key "$CWD")
58
+ export INSPECTOR_PROJECT_KEY="$PROJECT_KEY"
59
+
60
+ # File must live under repo root.
61
+ if [[ "$CANONICAL" != "$REPO_ROOT"/* && "$CANONICAL" != "$REPO_ROOT" ]]; then
62
+ export INSPECTOR_FILE_RELATIVE="$CANONICAL"
63
+ inspector_config_load "$REPO_ROOT"
64
+ inspector_config_enabled || exit 0
65
+ inspector_emit_whole_file_skipped "not_in_repo"
66
+ exit 0
67
+ fi
68
+ export INSPECTOR_FILE_RELATIVE="${CANONICAL#"$REPO_ROOT"/}"
69
+
70
+ inspector_config_load "$REPO_ROOT"
71
+
72
+ if ! inspector_config_enabled; then
73
+ # Silent skip — do not even emit an event when disabled. Disabled means
74
+ # "this plugin is dormant," not "this file was uninteresting."
75
+ exit 0
76
+ fi
77
+
78
+ # Excluded path containment check.
79
+ EXCLUDES=$(inspector_config_exclude_paths)
80
+ if [[ -n "$EXCLUDES" && "$EXCLUDES" != "null" && "$EXCLUDES" != "[]" ]]; then
81
+ if jq -e --arg rel "$INSPECTOR_FILE_RELATIVE" \
82
+ 'any(.[]; . as $p | $rel | startswith($p + "/") or . == $p or (("/" + $rel) | contains("/" + $p + "/")))' \
83
+ <<<"$EXCLUDES" >/dev/null 2>&1; then
84
+ inspector_emit_whole_file_skipped "excluded_path"
85
+ exit 0
86
+ fi
87
+ fi
88
+
89
+ # Look up checks for this file's extension. Use the *longest* matching suffix
90
+ # so `.test.ts` matches before `.ts`. For now this is a simple two-step:
91
+ # first the multi-dot suffix, then the simple extension.
92
+ FILE_BASE=$(basename "$CANONICAL")
93
+ EXT_LONG=""
94
+ EXT_SHORT=""
95
+ if [[ "$FILE_BASE" == *.*.* ]]; then
96
+ EXT_LONG=".${FILE_BASE#*.}"
97
+ fi
98
+ if [[ "$FILE_BASE" == *.* ]]; then
99
+ EXT_SHORT=".${FILE_BASE##*.}"
100
+ fi
101
+
102
+ CHECKS="[]"
103
+ if [[ -n "$EXT_LONG" ]]; then
104
+ CANDIDATE=$(inspector_config_checks_for_extension "$EXT_LONG")
105
+ if [[ -n "$CANDIDATE" && "$CANDIDATE" != "[]" ]]; then
106
+ CHECKS="$CANDIDATE"
107
+ fi
108
+ fi
109
+ if [[ "$CHECKS" == "[]" && -n "$EXT_SHORT" ]]; then
110
+ CANDIDATE=$(inspector_config_checks_for_extension "$EXT_SHORT")
111
+ if [[ -n "$CANDIDATE" && "$CANDIDATE" != "[]" ]]; then
112
+ CHECKS="$CANDIDATE"
113
+ fi
114
+ fi
115
+
116
+ if [[ "$CHECKS" == "[]" ]]; then
117
+ inspector_emit_whole_file_skipped "no_extension_match"
118
+ exit 0
119
+ fi
120
+
121
+ # Execute. Always exit 0 regardless of check outcomes.
122
+ inspector_run "$CHECKS" || true
123
+
124
+ exit 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
+ }