@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
@@ -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
+ }