@onlooker-community/ecosystem 0.28.0 → 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 +14 -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 +14 -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 +17 -5
- 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
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# N=5 parallel
|
|
2
|
+
# N=5 parallel `claude -p` evaluator for Compass.
|
|
3
3
|
#
|
|
4
|
-
# Launches N independent evaluator calls
|
|
5
|
-
# a decision (pass/fail) with
|
|
4
|
+
# Launches N independent evaluator calls via `claude -p --max-turns 1`,
|
|
5
|
+
# aggregates scores, and returns a decision (pass / fail / error) with
|
|
6
|
+
# confidence and stddev.
|
|
6
7
|
#
|
|
7
8
|
# Exposes:
|
|
8
9
|
# compass_evaluate <tool_name> <file_path> <operation> \
|
|
9
10
|
# <prior_turn> <context_excerpt> <session_id>
|
|
10
11
|
#
|
|
11
|
-
# Exits 0 if confidence >= threshold AND stddev <= stddev_threshold.
|
|
12
|
-
# Exits 1 if confidence < threshold OR stddev > stddev_threshold (block).
|
|
13
|
-
# Exits 2 on evaluator error (respects error_policy).
|
|
14
|
-
#
|
|
15
12
|
# Writes a JSON result object to stdout:
|
|
16
13
|
# {"decision":"pass|fail|error","confidence":<f>,"stddev":<f>,
|
|
17
14
|
# "primary_concern":"<str>","rationale":"<str>","sample_count":<n>}
|
|
15
|
+
#
|
|
16
|
+
# Exit codes:
|
|
17
|
+
# 0 pass (confidence >= threshold AND stddev <= stddev_threshold)
|
|
18
|
+
# 1 fail (block)
|
|
19
|
+
# 2 error (respects error_policy)
|
|
18
20
|
|
|
19
21
|
_COMPASS_EVAL_PROMPT_NO_PRIOR='You are evaluating whether a pending write operation has sufficient intent clarity.
|
|
20
22
|
|
|
@@ -78,87 +80,65 @@ path: FILE_PATH_PLACEHOLDER
|
|
|
78
80
|
operation: OPERATION_PLACEHOLDER
|
|
79
81
|
</tool_input>'
|
|
80
82
|
|
|
81
|
-
#
|
|
83
|
+
# Strip leading/trailing markdown fences a model occasionally emits.
|
|
84
|
+
_compass_strip_fences() {
|
|
85
|
+
printf '%s' "$1" | sed -e 's/^```json//' -e 's/^```//' -e 's/```$//'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Run a single evaluator call via `claude -p`. Writes JSON to $output_file.
|
|
82
89
|
# $1 — prompt text
|
|
83
90
|
# $2 — model
|
|
84
|
-
# $3 —
|
|
85
|
-
# $4 —
|
|
86
|
-
# $5 — output file path
|
|
87
|
-
# $6 — API key env var name (default: ANTHROPIC_API_KEY)
|
|
91
|
+
# $3 — timeout (seconds)
|
|
92
|
+
# $4 — output file path
|
|
88
93
|
_compass_run_single_eval() {
|
|
89
94
|
local prompt="$1"
|
|
90
95
|
local model="$2"
|
|
91
|
-
local
|
|
92
|
-
local
|
|
93
|
-
local output_file="$5"
|
|
94
|
-
local api_key_var="${6:-ANTHROPIC_API_KEY}"
|
|
95
|
-
local api_key="${!api_key_var:-}"
|
|
96
|
-
|
|
97
|
-
[[ -z "$api_key" ]] && { printf '{"error":"no_api_key"}' > "$output_file"; return 1; }
|
|
98
|
-
|
|
99
|
-
local request_body
|
|
100
|
-
request_body=$(jq -n \
|
|
101
|
-
--arg model "$model" \
|
|
102
|
-
--argjson temp "$temperature" \
|
|
103
|
-
--argjson max_tokens "$max_tokens" \
|
|
104
|
-
--arg prompt "$prompt" \
|
|
105
|
-
'{
|
|
106
|
-
model: $model,
|
|
107
|
-
max_tokens: $max_tokens,
|
|
108
|
-
temperature: $temp,
|
|
109
|
-
messages: [{"role": "user", "content": $prompt}]
|
|
110
|
-
}' 2>/dev/null) || { printf '{"error":"request_build_failed"}' > "$output_file"; return 1; }
|
|
111
|
-
|
|
112
|
-
local http_response http_code response_body
|
|
113
|
-
http_response=$(curl -s -w '\n%{http_code}' \
|
|
114
|
-
-X POST "https://api.anthropic.com/v1/messages" \
|
|
115
|
-
-H "x-api-key: ${api_key}" \
|
|
116
|
-
-H "anthropic-version: 2023-06-01" \
|
|
117
|
-
-H "content-type: application/json" \
|
|
118
|
-
-d "$request_body" \
|
|
119
|
-
--max-time 15 \
|
|
120
|
-
2>/dev/null) || { printf '{"error":"curl_failed"}' > "$output_file"; return 1; }
|
|
121
|
-
|
|
122
|
-
http_code=$(printf '%s' "$http_response" | tail -n1)
|
|
123
|
-
response_body=$(printf '%s' "$http_response" | head -n -1)
|
|
124
|
-
|
|
125
|
-
if [[ "$http_code" == "429" ]]; then
|
|
126
|
-
sleep 2
|
|
127
|
-
http_response=$(curl -s -w '\n%{http_code}' \
|
|
128
|
-
-X POST "https://api.anthropic.com/v1/messages" \
|
|
129
|
-
-H "x-api-key: ${api_key}" \
|
|
130
|
-
-H "anthropic-version: 2023-06-01" \
|
|
131
|
-
-H "content-type: application/json" \
|
|
132
|
-
-d "$request_body" \
|
|
133
|
-
--max-time 15 \
|
|
134
|
-
2>/dev/null) || { printf '{"error":"curl_failed_retry"}' > "$output_file"; return 1; }
|
|
135
|
-
http_code=$(printf '%s' "$http_response" | tail -n1)
|
|
136
|
-
response_body=$(printf '%s' "$http_response" | head -n -1)
|
|
137
|
-
fi
|
|
96
|
+
local timeout_secs="$3"
|
|
97
|
+
local output_file="$4"
|
|
138
98
|
|
|
139
|
-
if
|
|
140
|
-
printf '{"error":"
|
|
99
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
100
|
+
printf '{"error":"claude_cli_missing"}' > "$output_file"
|
|
141
101
|
return 1
|
|
142
102
|
fi
|
|
143
103
|
|
|
144
|
-
local
|
|
145
|
-
|
|
146
|
-
|
|
104
|
+
local prompt_file
|
|
105
|
+
prompt_file=$(mktemp -t compass-prompt.XXXXXX 2>/dev/null) || prompt_file="/tmp/compass-prompt.$$.${RANDOM}"
|
|
106
|
+
printf '%s' "$prompt" > "$prompt_file"
|
|
107
|
+
|
|
108
|
+
local args=(-p --max-turns 1)
|
|
109
|
+
[[ -n "$model" ]] && args+=(--model "$model")
|
|
110
|
+
|
|
111
|
+
local response=""
|
|
112
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
113
|
+
response=$(COMPASS_NESTED=1 timeout "$timeout_secs" claude "${args[@]}" <"$prompt_file" 2>/dev/null) || response=""
|
|
114
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
115
|
+
response=$(COMPASS_NESTED=1 gtimeout "$timeout_secs" claude "${args[@]}" <"$prompt_file" 2>/dev/null) || response=""
|
|
116
|
+
else
|
|
117
|
+
response=$(COMPASS_NESTED=1 claude "${args[@]}" <"$prompt_file" 2>/dev/null) || response=""
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
rm -f "$prompt_file" 2>/dev/null || true
|
|
121
|
+
|
|
122
|
+
if [[ -z "$response" ]]; then
|
|
123
|
+
printf '{"error":"empty_response"}' > "$output_file"
|
|
147
124
|
return 1
|
|
148
|
-
|
|
125
|
+
fi
|
|
149
126
|
|
|
150
|
-
|
|
127
|
+
local clean
|
|
128
|
+
clean=$(_compass_strip_fences "$response")
|
|
129
|
+
|
|
130
|
+
# Confirm the model returned a JSON object with a numeric score.
|
|
151
131
|
local score
|
|
152
|
-
score=$(printf '%s' "$
|
|
132
|
+
score=$(printf '%s' "$clean" | jq -r '.score // empty' 2>/dev/null) || score=""
|
|
153
133
|
if [[ -z "$score" ]]; then
|
|
154
134
|
printf '{"error":"invalid_json_response"}' > "$output_file"
|
|
155
135
|
return 1
|
|
156
136
|
fi
|
|
157
137
|
|
|
158
|
-
printf '%s' "$
|
|
138
|
+
printf '%s' "$clean" > "$output_file"
|
|
159
139
|
}
|
|
160
140
|
|
|
161
|
-
# Build the evaluator prompt.
|
|
141
|
+
# Build the evaluator prompt by interpolating the data slots.
|
|
162
142
|
_compass_build_prompt() {
|
|
163
143
|
local prior_turn="$1"
|
|
164
144
|
local context_excerpt="$2"
|
|
@@ -182,7 +162,7 @@ _compass_build_prompt() {
|
|
|
182
162
|
printf '%s' "$template"
|
|
183
163
|
}
|
|
184
164
|
|
|
185
|
-
#
|
|
165
|
+
# Mean of space-separated floats.
|
|
186
166
|
_compass_mean() {
|
|
187
167
|
local scores=("$@")
|
|
188
168
|
local n="${#scores[@]}"
|
|
@@ -195,7 +175,7 @@ _compass_mean() {
|
|
|
195
175
|
awk "BEGIN {printf \"%.4f\", $sum / $n}" 2>/dev/null || printf '0'
|
|
196
176
|
}
|
|
197
177
|
|
|
198
|
-
#
|
|
178
|
+
# Population stddev of space-separated floats.
|
|
199
179
|
_compass_stddev() {
|
|
200
180
|
local scores=("$@")
|
|
201
181
|
local n="${#scores[@]}"
|
|
@@ -213,7 +193,7 @@ _compass_stddev() {
|
|
|
213
193
|
# Main evaluator entry point.
|
|
214
194
|
# $1 — tool_name
|
|
215
195
|
# $2 — file_path
|
|
216
|
-
# $3 — operation (write|edit|multi_edit|
|
|
196
|
+
# $3 — operation (write|edit|multi_edit|bash_write)
|
|
217
197
|
# $4 — prior_turn (may be empty)
|
|
218
198
|
# $5 — context_excerpt
|
|
219
199
|
# $6 — session_id
|
|
@@ -225,17 +205,11 @@ compass_evaluate() {
|
|
|
225
205
|
local context_excerpt="$5"
|
|
226
206
|
local session_id="${6:-unknown}"
|
|
227
207
|
|
|
228
|
-
local model
|
|
208
|
+
local model n_samples timeout_secs min_valid
|
|
229
209
|
model=$(compass_config_get '.compass.evaluator.model')
|
|
230
210
|
model="${model:-claude-haiku-4-5-20251001}"
|
|
231
|
-
|
|
232
|
-
local n_samples temperature max_tokens timeout_secs min_valid
|
|
233
211
|
n_samples=$(compass_config_get '.compass.evaluator.n')
|
|
234
212
|
n_samples="${n_samples:-5}"
|
|
235
|
-
temperature=$(compass_config_get '.compass.evaluator.temperature')
|
|
236
|
-
temperature="${temperature:-0.3}"
|
|
237
|
-
max_tokens=$(compass_config_get '.compass.evaluator.max_output_tokens')
|
|
238
|
-
max_tokens="${max_tokens:-128}"
|
|
239
213
|
timeout_secs=$(compass_config_get '.compass.evaluator.sample_timeout_seconds')
|
|
240
214
|
timeout_secs="${timeout_secs:-8}"
|
|
241
215
|
min_valid=$(compass_config_get '.compass.evaluator.min_valid_samples')
|
|
@@ -250,34 +224,22 @@ compass_evaluate() {
|
|
|
250
224
|
local prompt
|
|
251
225
|
prompt=$(_compass_build_prompt "$prior_turn" "$context_excerpt" "$tool_name" "$file_path" "$operation")
|
|
252
226
|
|
|
253
|
-
# Launch N parallel eval calls.
|
|
254
227
|
local tmp_dir
|
|
255
|
-
tmp_dir=$(mktemp -d -t compass-eval.XXXXXX 2>/dev/null) || tmp_dir="/tmp/compass-eval
|
|
228
|
+
tmp_dir=$(mktemp -d -t compass-eval.XXXXXX 2>/dev/null) || tmp_dir="/tmp/compass-eval.$$.${RANDOM}"
|
|
256
229
|
mkdir -p "$tmp_dir"
|
|
257
230
|
|
|
258
231
|
local pids=()
|
|
259
232
|
local i
|
|
260
233
|
for (( i=0; i<n_samples; i++ )); do
|
|
261
234
|
local out_file="${tmp_dir}/sample_${i}.json"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"$prompt" "$model" "$temperature" "$max_tokens" "$out_file"
|
|
265
|
-
) &
|
|
235
|
+
_compass_run_single_eval \
|
|
236
|
+
"$prompt" "$model" "$timeout_secs" "$out_file" &
|
|
266
237
|
pids+=($!)
|
|
267
238
|
done
|
|
268
239
|
|
|
269
|
-
# Collect with timeout watchdog.
|
|
270
|
-
local deadline=$(( $(date +%s) + timeout_secs ))
|
|
271
240
|
local pid
|
|
272
241
|
for pid in "${pids[@]}"; do
|
|
273
|
-
|
|
274
|
-
now=$(date +%s)
|
|
275
|
-
local remaining=$(( deadline - now ))
|
|
276
|
-
if [[ "$remaining" -gt 0 ]]; then
|
|
277
|
-
wait "$pid" 2>/dev/null || true
|
|
278
|
-
else
|
|
279
|
-
kill "$pid" 2>/dev/null || true
|
|
280
|
-
fi
|
|
242
|
+
wait "$pid" 2>/dev/null || true
|
|
281
243
|
done
|
|
282
244
|
|
|
283
245
|
# Aggregate valid scores.
|
|
@@ -307,9 +269,7 @@ compass_evaluate() {
|
|
|
307
269
|
error_policy="${error_policy:-closed}"
|
|
308
270
|
|
|
309
271
|
local decision="error"
|
|
310
|
-
|
|
311
|
-
decision="pass"
|
|
312
|
-
fi
|
|
272
|
+
[[ "$error_policy" == "open" ]] && decision="pass"
|
|
313
273
|
|
|
314
274
|
jq -n \
|
|
315
275
|
--arg decision "$decision" \
|
|
@@ -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
|
+
}
|