@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
|
@@ -107,3 +107,128 @@ setup() {
|
|
|
107
107
|
and .schema_version == "1.0"' \
|
|
108
108
|
>/dev/null
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
# ----------------------------------------------------------------------------
|
|
112
|
+
# Hook health instrumentation: hook_register / hook_success / hook_failure
|
|
113
|
+
# ----------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
@test "hook_register seeds hook name and start time" {
|
|
116
|
+
hook_register "my-hook" "My Hook" "A description"
|
|
117
|
+
trap - EXIT # disarm the trap hook_register installed so it can't fire later
|
|
118
|
+
[ "${_HOOK_NAME}" = "my-hook" ]
|
|
119
|
+
[ -n "${_HOOK_START_TIME}" ]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@test "hook_success writes a success record to the hook-health log" {
|
|
123
|
+
export _HOOK_SESSION_ID="health-success-session"
|
|
124
|
+
hook_register "success-hook"
|
|
125
|
+
hook_success
|
|
126
|
+
[ -f "$ONLOOKER_HOOK_HEALTH_LOG" ]
|
|
127
|
+
tail -n 1 "$ONLOOKER_HOOK_HEALTH_LOG" | jq -e \
|
|
128
|
+
'.hook == "success-hook"
|
|
129
|
+
and .status == "success"
|
|
130
|
+
and .error == null
|
|
131
|
+
and .session_id == "health-success-session"' \
|
|
132
|
+
>/dev/null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@test "hook_failure writes a failure record with the error message" {
|
|
136
|
+
hook_register "failure-hook"
|
|
137
|
+
hook_failure "boom: it broke"
|
|
138
|
+
[ -f "$ONLOOKER_HOOK_HEALTH_LOG" ]
|
|
139
|
+
tail -n 1 "$ONLOOKER_HOOK_HEALTH_LOG" | jq -e \
|
|
140
|
+
'.hook == "failure-hook"
|
|
141
|
+
and .status == "failure"
|
|
142
|
+
and .error == "boom: it broke"' \
|
|
143
|
+
>/dev/null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@test "hook_health_summary reflects seeded success and failure records" {
|
|
147
|
+
# Two records for the same hook: one success, one failure.
|
|
148
|
+
hook_register "summary-hook"
|
|
149
|
+
hook_success
|
|
150
|
+
hook_register "summary-hook"
|
|
151
|
+
hook_failure "an error"
|
|
152
|
+
|
|
153
|
+
local summary
|
|
154
|
+
summary=$(hook_health_summary 24)
|
|
155
|
+
echo "$summary" | jq -e \
|
|
156
|
+
'map(select(.hook == "summary-hook"))
|
|
157
|
+
| .[0]
|
|
158
|
+
| .total == 2
|
|
159
|
+
and .success == 1
|
|
160
|
+
and .failure == 1
|
|
161
|
+
and .last_error == "an error"' \
|
|
162
|
+
>/dev/null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ----------------------------------------------------------------------------
|
|
166
|
+
# Hook composition bus: hook_bus_list / hook_bus_cleanup
|
|
167
|
+
# ----------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
@test "hook_bus_list lists put findings without the .json extension" {
|
|
170
|
+
export _HOOK_SESSION_ID="bus-list-session"
|
|
171
|
+
export _HOOK_TOOL_NAME="Agent"
|
|
172
|
+
hook_bus_init '{"tool_input":{"agent_id":"list"}}'
|
|
173
|
+
hook_bus_put "alpha" '{"a":1}'
|
|
174
|
+
hook_bus_put "beta" '{"b":2}'
|
|
175
|
+
local listing
|
|
176
|
+
listing=$(hook_bus_list | sort | tr '\n' ' ')
|
|
177
|
+
[ "$listing" = "alpha beta " ]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@test "hook_bus_cleanup removes aged bus dirs but keeps fresh ones" {
|
|
181
|
+
local tmp_dir
|
|
182
|
+
tmp_dir="$(cd /tmp && pwd -P)"
|
|
183
|
+
local fresh="${tmp_dir}/.onlooker-hook-bus-cleanup-fresh-$$"
|
|
184
|
+
local aged="${tmp_dir}/.onlooker-hook-bus-cleanup-aged-$$"
|
|
185
|
+
mkdir -p "$fresh" "$aged"
|
|
186
|
+
# Backdate the aged dir well past the 5-minute (-mmin +5) cutoff.
|
|
187
|
+
touch -t "$(date -v-10M +%Y%m%d%H%M.%S 2>/dev/null || date -d '10 minutes ago' +%Y%m%d%H%M.%S)" "$aged"
|
|
188
|
+
|
|
189
|
+
hook_bus_cleanup
|
|
190
|
+
|
|
191
|
+
[ ! -d "$aged" ]
|
|
192
|
+
[ -d "$fresh" ]
|
|
193
|
+
rm -rf "$fresh"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# ----------------------------------------------------------------------------
|
|
197
|
+
# Readability / writability validators
|
|
198
|
+
# ----------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
@test "validate_file_readable succeeds for existing readable file" {
|
|
201
|
+
local f="${BATS_TEST_TMPDIR}/readable.txt"
|
|
202
|
+
touch "$f"
|
|
203
|
+
validate_file_readable "$f"
|
|
204
|
+
[ "$?" -eq 0 ]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@test "validate_file_readable fails for missing file" {
|
|
208
|
+
! validate_file_readable "${BATS_TEST_TMPDIR}/no-such-file.txt"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@test "validate_file_writable succeeds when parent directory is writable" {
|
|
212
|
+
validate_file_writable "${BATS_TEST_TMPDIR}/new-file.txt"
|
|
213
|
+
[ "$?" -eq 0 ]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@test "validate_file_writable fails when parent directory does not exist" {
|
|
217
|
+
! validate_file_writable "${BATS_TEST_TMPDIR}/missing-dir/new-file.txt"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# ----------------------------------------------------------------------------
|
|
221
|
+
# Turn state tracking: turn_state_next_turn
|
|
222
|
+
# ----------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
@test "turn_state_next_turn increments turn_number from 1 to 2" {
|
|
225
|
+
local session_id="next-turn-session"
|
|
226
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
227
|
+
turn_state_ensure_session "$session_id"
|
|
228
|
+
[ -f "$tracker" ]
|
|
229
|
+
# Fresh session starts at turn_number 1.
|
|
230
|
+
jq -e '.turn_number == 1' "$tracker" >/dev/null
|
|
231
|
+
|
|
232
|
+
turn_state_next_turn "$session_id"
|
|
233
|
+
jq -e '.turn_number == 2 and .turn_tool_seq == 0' "$tracker" >/dev/null
|
|
234
|
+
}
|
|
@@ -127,3 +127,170 @@ setup() {
|
|
|
127
127
|
and (.payload.command | test("worktree:create"))' \
|
|
128
128
|
>/dev/null
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
@test "worktree_tracker_repo_root prints the git toplevel for a cwd inside the repo" {
|
|
132
|
+
local expected
|
|
133
|
+
expected=$(git -C "$GIT_REPO" rev-parse --show-toplevel)
|
|
134
|
+
|
|
135
|
+
run worktree_tracker_repo_root "$GIT_REPO"
|
|
136
|
+
[ "$status" -eq 0 ]
|
|
137
|
+
[ "$output" = "$expected" ]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@test "worktree_tracker_repo_root returns empty for a non-repo directory" {
|
|
141
|
+
local non_repo="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
142
|
+
mkdir -p "$non_repo"
|
|
143
|
+
|
|
144
|
+
run worktree_tracker_repo_root "$non_repo"
|
|
145
|
+
[ "$status" -ne 0 ]
|
|
146
|
+
[ -z "$output" ]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@test "worktree_tracker_repo_root returns non-zero for empty cwd" {
|
|
150
|
+
run worktree_tracker_repo_root ""
|
|
151
|
+
[ "$status" -ne 0 ]
|
|
152
|
+
[ -z "$output" ]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@test "worktree_tracker_git_create creates a worktree at the expected path" {
|
|
156
|
+
local name="feature-create"
|
|
157
|
+
local expected="${GIT_REPO}/.claude/worktrees/${name}"
|
|
158
|
+
|
|
159
|
+
local worktree_path
|
|
160
|
+
worktree_path=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
161
|
+
|
|
162
|
+
[ "$worktree_path" = "$(cd "$expected" && pwd -P)" ]
|
|
163
|
+
[ -d "$worktree_path" ]
|
|
164
|
+
git -C "$GIT_REPO" worktree list --porcelain | grep -Fq "worktree $(cd "$expected" && pwd -P)"
|
|
165
|
+
git -C "$GIT_REPO" show-ref --verify --quiet "refs/heads/worktree-${name}"
|
|
166
|
+
|
|
167
|
+
worktree_tracker_git_remove "$GIT_REPO" "$worktree_path" 2>/dev/null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@test "worktree_tracker_git_create is idempotent for an existing worktree dir" {
|
|
171
|
+
local name="feature-idempotent"
|
|
172
|
+
|
|
173
|
+
local first second
|
|
174
|
+
first=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
175
|
+
second=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
176
|
+
|
|
177
|
+
[ "$first" = "$second" ]
|
|
178
|
+
[ -d "$second" ]
|
|
179
|
+
|
|
180
|
+
worktree_tracker_git_remove "$GIT_REPO" "$first" 2>/dev/null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@test "worktree_tracker_git_create returns non-zero with missing args" {
|
|
184
|
+
run worktree_tracker_git_create "$GIT_REPO" ""
|
|
185
|
+
[ "$status" -ne 0 ]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@test "worktree_tracker_git_remove removes a registered worktree" {
|
|
189
|
+
local name="feature-remove"
|
|
190
|
+
local worktree_path
|
|
191
|
+
worktree_path=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
192
|
+
[ -d "$worktree_path" ]
|
|
193
|
+
|
|
194
|
+
worktree_tracker_git_remove "$GIT_REPO" "$worktree_path" 2>/dev/null
|
|
195
|
+
|
|
196
|
+
[ ! -d "$worktree_path" ]
|
|
197
|
+
! git -C "$GIT_REPO" worktree list --porcelain | grep -Fq "worktree ${worktree_path}"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "worktree_tracker_record_created writes timing into the session tracker" {
|
|
201
|
+
local session_id="worktree-record-001"
|
|
202
|
+
local name="feature-record"
|
|
203
|
+
local worktree_path="${GIT_REPO}/.claude/worktrees/${name}"
|
|
204
|
+
local branch="worktree-${name}"
|
|
205
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
206
|
+
|
|
207
|
+
worktree_tracker_record_created "$session_id" "$name" "$worktree_path" "$branch"
|
|
208
|
+
|
|
209
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
210
|
+
[ -f "$tracker_file" ]
|
|
211
|
+
jq -e \
|
|
212
|
+
--arg name "$name" \
|
|
213
|
+
--arg path "$worktree_path" \
|
|
214
|
+
--arg branch "$branch" \
|
|
215
|
+
'.worktrees[$name].path == $path
|
|
216
|
+
and .worktrees[$name].branch == $branch
|
|
217
|
+
and (.worktrees[$name].start_time_ms | type == "number")' \
|
|
218
|
+
"$tracker_file" >/dev/null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@test "worktree_tracker_record_created is a no-op with missing args" {
|
|
222
|
+
local session_id="worktree-record-noop"
|
|
223
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
224
|
+
|
|
225
|
+
run worktree_tracker_record_created "$session_id" "" "/some/path" "branch"
|
|
226
|
+
[ "$status" -eq 0 ]
|
|
227
|
+
[ ! -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}" ]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@test "worktree_tracker_duration_ms returns elapsed ms from a seeded start" {
|
|
231
|
+
local session_id="worktree-duration-001"
|
|
232
|
+
local name="feature-duration"
|
|
233
|
+
local worktree_path="${GIT_REPO}/.claude/worktrees/${name}"
|
|
234
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
235
|
+
|
|
236
|
+
worktree_tracker_record_created "$session_id" "$name" "$worktree_path" "worktree-${name}"
|
|
237
|
+
|
|
238
|
+
# Rewind the recorded start_time_ms by ~2s so elapsed is a stable positive value.
|
|
239
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
240
|
+
local now_ms past temp
|
|
241
|
+
now_ms=$(session_tracker_now_ms)
|
|
242
|
+
past=$(( now_ms - 2000 ))
|
|
243
|
+
temp=$(mktemp)
|
|
244
|
+
jq --arg name "$name" --argjson ms "$past" \
|
|
245
|
+
'.worktrees[$name].start_time_ms = $ms' "$tracker_file" >"$temp"
|
|
246
|
+
mv "$temp" "$tracker_file"
|
|
247
|
+
|
|
248
|
+
local duration
|
|
249
|
+
duration=$(worktree_tracker_duration_ms "$session_id" "$worktree_path")
|
|
250
|
+
[[ "$duration" =~ ^[0-9]+$ ]]
|
|
251
|
+
[ "$duration" -ge 1900 ]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@test "worktree_tracker_duration_ms returns empty for an unknown worktree path" {
|
|
255
|
+
local session_id="worktree-duration-unknown"
|
|
256
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
257
|
+
turn_state_ensure_session "$session_id"
|
|
258
|
+
|
|
259
|
+
run worktree_tracker_duration_ms "$session_id" "/never/recorded"
|
|
260
|
+
[ "$status" -eq 0 ]
|
|
261
|
+
[ -z "$output" ]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@test "worktree_tracker_clear_by_path removes the recorded entry" {
|
|
265
|
+
local session_id="worktree-clear-001"
|
|
266
|
+
local name="feature-clear"
|
|
267
|
+
local worktree_path="${GIT_REPO}/.claude/worktrees/${name}"
|
|
268
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
269
|
+
|
|
270
|
+
worktree_tracker_record_created "$session_id" "$name" "$worktree_path" "worktree-${name}"
|
|
271
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
272
|
+
jq -e --arg name "$name" '.worktrees | has($name)' "$tracker_file" >/dev/null
|
|
273
|
+
|
|
274
|
+
worktree_tracker_clear_by_path "$session_id" "$worktree_path"
|
|
275
|
+
|
|
276
|
+
jq -e --arg name "$name" '(.worktrees | has($name)) | not' "$tracker_file" >/dev/null
|
|
277
|
+
run worktree_tracker_duration_ms "$session_id" "$worktree_path"
|
|
278
|
+
[ -z "$output" ]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@test "worktree_tracker_append lands a JSON line in the session history" {
|
|
282
|
+
local session_id="worktree-append-001"
|
|
283
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/${session_id}.jsonl"
|
|
284
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
285
|
+
|
|
286
|
+
local event
|
|
287
|
+
event=$(jq -c -n '{event_type: "tool.shell.exec", payload: {command: "git worktree:create"}}')
|
|
288
|
+
|
|
289
|
+
worktree_tracker_append "$session_id" "$event"
|
|
290
|
+
|
|
291
|
+
[ -f "$history_file" ]
|
|
292
|
+
tail -n 1 "$history_file" | jq -e \
|
|
293
|
+
'.event_type == "tool.shell.exec"
|
|
294
|
+
and (.payload.command | test("worktree:create"))' \
|
|
295
|
+
>/dev/null
|
|
296
|
+
}
|