@onlooker-community/ecosystem 0.43.3 → 0.43.4
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/plugin.json +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/docs/architecture.md +3 -1
- package/package.json +1 -1
- package/scripts/hooks/prompt-rule-injector.sh +10 -2
- package/scripts/lib/prompt-rules.sh +43 -12
- package/test/bats/prompt-rules.bats +61 -24
- package/test/bus-coverage.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.4",
|
|
4
4
|
"description": "Observability substrate for Claude Code. Provides the shared $ONLOOKER_DIR storage root (default $HOME/.onlooker), canonical schema-validated event emission, session and tool tracking hooks, and prompt rules. Required by all other Onlooker plugins.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.43.4](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.3...ecosystem-v0.43.4) (2026-08-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **events:** route prompt_rule emission through the canonical emitter :relieved: ([#196](https://github.com/onlooker-community/ecosystem/issues/196)) ([c233bfa](https://github.com/onlooker-community/ecosystem/commit/c233bfa466cf400e767f4673e0143081618314a7))
|
|
9
|
+
|
|
3
10
|
## [0.43.3](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.2...ecosystem-v0.43.3) (2026-08-22)
|
|
4
11
|
|
|
5
12
|
|
package/docs/architecture.md
CHANGED
|
@@ -93,7 +93,9 @@ Every observable event flows through `onlooker-event.mjs` before being written t
|
|
|
93
93
|
|
|
94
94
|
The schema is versioned independently and published to npm. Plugin shell scripts invoke `onlooker-event.mjs` at runtime so schema validation always reflects the installed version.
|
|
95
95
|
|
|
96
|
-
> **Note:**
|
|
96
|
+
> **Note:** Every emission path that fires in normal operation routes through `onlooker-event.mjs`. `prompt_rule.*` was the last routine exception — `prompt_rules_emit` hand-built its envelope with `jq` and wrote straight to `$ONLOOKER_EVENTS_LOG`, so those lines carried none of the required `id`/`schema_version`/`runtime`/`machine_id`/`sequence` fields and added a `turn` field the envelope forbids. It now goes through the emitter like everything else.
|
|
97
|
+
>
|
|
98
|
+
> One hand-built envelope remains, in the `safe_emit` fallback in `scripts/lib/validate-path.sh`. It is reached only when `$ONLOOKER_EMIT` is missing, which does not happen in a normal checkout or install, so it is latent rather than live — but if it ever fires it writes the same unvalidatable shape. Route new event types through the emitter rather than writing to the log directly: an envelope assembled anywhere else will drift from the schema without anything noticing.
|
|
97
99
|
|
|
98
100
|
### Emission gates
|
|
99
101
|
|
package/package.json
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
# and injects guidance for rules whose POSIX-ERE pattern matches the prompt.
|
|
7
7
|
# Each rule fires at most once per session per rule_id.
|
|
8
8
|
#
|
|
9
|
-
# Emits canonical
|
|
9
|
+
# Emits canonical events to ~/.onlooker/logs/onlooker-events.jsonl:
|
|
10
10
|
# prompt_rule.matched — every match (including subsequent matches in-session)
|
|
11
11
|
# prompt_rule.applied — only when guidance is actually injected
|
|
12
|
+
# Both route through onlooker-event.mjs and are schema-validated; a rejected
|
|
13
|
+
# event is dropped rather than written, and never blocks prompt submission.
|
|
12
14
|
#
|
|
13
15
|
# Usage:
|
|
14
16
|
# echo "$INPUT" | prompt-rule-injector.sh
|
|
@@ -74,8 +76,14 @@ while IFS= read -r rule; do
|
|
|
74
76
|
continue
|
|
75
77
|
fi
|
|
76
78
|
|
|
79
|
+
# match_type and trigger_source are required by the prompt_rule.matched
|
|
80
|
+
# payload schema. Both are fixed for this hook rather than derived: rules
|
|
81
|
+
# match with bash `[[ =~ ]]`, which is POSIX ERE, and the only thing this
|
|
82
|
+
# UserPromptSubmit hook ever matches against is the prompt itself. A future
|
|
83
|
+
# vocabulary or file-path matcher would set them from the rule.
|
|
77
84
|
prompt_rules_emit "$SESSION_ID" "prompt_rule.matched" \
|
|
78
|
-
"$(jq -cn --arg id "$RULE_ID"
|
|
85
|
+
"$(jq -cn --arg id "$RULE_ID" \
|
|
86
|
+
'{rule_id: $id, match_type: "regex", trigger_source: "prompt"}')" || true
|
|
79
87
|
|
|
80
88
|
ALREADY_FIRED=$(echo "$FIRED" | jq --arg id "$RULE_ID" 'index($id) != null' 2>/dev/null)
|
|
81
89
|
if [[ "$FIRE_ONCE" == "true" && "$ALREADY_FIRED" == "true" ]]; then
|
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
export ONLOOKER_PROMPT_RULES_DIR="${ONLOOKER_PROMPT_RULES_DIR:-$ONLOOKER_DIR/prompt-rules}"
|
|
27
27
|
export ONLOOKER_PROMPT_RULES_SESSIONS_DIR="$ONLOOKER_PROMPT_RULES_DIR/sessions"
|
|
28
28
|
|
|
29
|
+
# The canonical emitter is a sibling file, so resolve it from this script's own
|
|
30
|
+
# location rather than from a caller-supplied $CLAUDE_PLUGIN_ROOT. Sourcing this
|
|
31
|
+
# lib from a scope that never set that variable then still yields a working
|
|
32
|
+
# emit path instead of a silent no-op.
|
|
33
|
+
_PROMPT_RULES_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
34
|
+
_PROMPT_RULES_EVENT_JS="$_PROMPT_RULES_LIB_DIR/onlooker-event.mjs"
|
|
35
|
+
|
|
29
36
|
# Path to the global rules file.
|
|
30
37
|
# Usage: path=$(prompt_rules_global_path)
|
|
31
38
|
prompt_rules_global_path() {
|
|
@@ -142,8 +149,24 @@ prompt_rules_pattern_matches() {
|
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
# Append a prompt-rule event to the global events log.
|
|
145
|
-
#
|
|
146
|
-
#
|
|
152
|
+
#
|
|
153
|
+
# Routes through onlooker-event.mjs like every other emit path in the repo, so
|
|
154
|
+
# the envelope is built once, in one place, and validated against
|
|
155
|
+
# @onlooker-community/schema wherever that package resolves (dev, CI, tests).
|
|
156
|
+
#
|
|
157
|
+
# This used to hand-build a five-field envelope with jq. That envelope was
|
|
158
|
+
# never valid: it omitted every required id/schema_version/runtime/machine_id/
|
|
159
|
+
# sequence field, and it added a `turn` field the envelope forbids outright
|
|
160
|
+
# (event.v1.json is additionalProperties:false and has no `turn`). The canonical
|
|
161
|
+
# emitter carries a turn as `turn_number` INSIDE the payload, and neither
|
|
162
|
+
# prompt_rule payload schema has that property, so a turn number has nowhere
|
|
163
|
+
# valid to go on these two event types and is no longer recorded.
|
|
164
|
+
#
|
|
165
|
+
# Fails closed: if the emitter is missing or rejects the payload, nothing is
|
|
166
|
+
# written. A dropped telemetry event beats an unparseable line on the bus, and
|
|
167
|
+
# both callers in prompt-rule-injector.sh already discard the return with
|
|
168
|
+
# `|| true` so a rejection can never block prompt submission.
|
|
169
|
+
#
|
|
147
170
|
# Usage: prompt_rules_emit "$session_id" "prompt_rule.matched" "$payload_json"
|
|
148
171
|
prompt_rules_emit() {
|
|
149
172
|
local session_id="${1:-unknown}"
|
|
@@ -155,22 +178,30 @@ prompt_rules_emit() {
|
|
|
155
178
|
local payload_json="${3:-}"
|
|
156
179
|
[ -z "$payload_json" ] && payload_json='{}'
|
|
157
180
|
[[ -z "$event_type" ]] && return 1
|
|
158
|
-
ensure_file_exists "$ONLOOKER_EVENTS_LOG" || return 1
|
|
159
181
|
|
|
160
|
-
local
|
|
161
|
-
|
|
182
|
+
local event_js="$_PROMPT_RULES_EVENT_JS"
|
|
183
|
+
[[ -n "${_ONLOOKER_EVENT_JS:-}" && -f "${_ONLOOKER_EVENT_JS}" ]] && event_js="$_ONLOOKER_EVENT_JS"
|
|
184
|
+
[[ -f "$event_js" ]] || return 1
|
|
185
|
+
|
|
186
|
+
local plugin
|
|
162
187
|
plugin="${ONLOOKER_PLUGIN_NAME:-onlooker}"
|
|
163
188
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
--arg sid "$session_id" \
|
|
189
|
+
local params
|
|
190
|
+
params=$(jq -cn \
|
|
167
191
|
--arg plugin "$plugin" \
|
|
192
|
+
--arg sid "$session_id" \
|
|
168
193
|
--arg type "$event_type" \
|
|
169
194
|
--argjson payload "$payload_json" \
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
'{plugin: $plugin, session_id: $sid, event_type: $type, payload: $payload}') || return 1
|
|
196
|
+
|
|
197
|
+
local event
|
|
198
|
+
event=$(printf '%s' "$params" \
|
|
199
|
+
| ONLOOKER_DIR="$ONLOOKER_DIR" ONLOOKER_PLUGIN_NAME="$plugin" \
|
|
200
|
+
node "$event_js" emit 2>/dev/null) || return 1
|
|
201
|
+
[[ -z "$event" ]] && return 1
|
|
202
|
+
|
|
203
|
+
ensure_file_exists "$ONLOOKER_EVENTS_LOG" || return 1
|
|
204
|
+
printf '%s\n' "$event" >> "$ONLOOKER_EVENTS_LOG"
|
|
174
205
|
}
|
|
175
206
|
|
|
176
207
|
# Print a human-readable table of merged rules with their fired status.
|
|
@@ -295,9 +295,13 @@ write_project_rules() {
|
|
|
295
295
|
[ "$path" = "$expected" ]
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
# The canonical prompt_rule.matched payload. Both extra fields are required by
|
|
299
|
+
# the schema, so every positive matched-event test has to carry them.
|
|
300
|
+
MATCHED_PAYLOAD='{"rule_id":"rule-1","match_type":"regex","trigger_source":"prompt"}'
|
|
301
|
+
|
|
298
302
|
@test "emit: appends a JSON event line with type, session, payload, and plugin" {
|
|
299
303
|
: >"$ONLOOKER_EVENTS_LOG"
|
|
300
|
-
run prompt_rules_emit "sess-emit" "prompt_rule.matched"
|
|
304
|
+
run prompt_rules_emit "sess-emit" "prompt_rule.matched" "$MATCHED_PAYLOAD"
|
|
301
305
|
[ "$status" -eq 0 ]
|
|
302
306
|
|
|
303
307
|
local line
|
|
@@ -309,59 +313,92 @@ write_project_rules() {
|
|
|
309
313
|
[ "$(echo "$line" | jq -r '.plugin')" = "onlooker" ]
|
|
310
314
|
}
|
|
311
315
|
|
|
312
|
-
@test "emit:
|
|
316
|
+
@test "emit: writes a full canonical envelope, not a hand-built subset" {
|
|
313
317
|
: >"$ONLOOKER_EVENTS_LOG"
|
|
314
|
-
|
|
318
|
+
prompt_rules_emit "sess-envelope" "prompt_rule.matched" "$MATCHED_PAYLOAD"
|
|
315
319
|
|
|
316
320
|
local line
|
|
317
321
|
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
318
|
-
|
|
322
|
+
# The five fields the old jq-built envelope omitted. Each is required by
|
|
323
|
+
# event.v1.json, so their absence is what made those lines unvalidatable.
|
|
324
|
+
[ "$(echo "$line" | jq -r 'has("id")')" = "true" ]
|
|
325
|
+
[ "$(echo "$line" | jq -r 'has("schema_version")')" = "true" ]
|
|
326
|
+
[ "$(echo "$line" | jq -r 'has("runtime")')" = "true" ]
|
|
327
|
+
[ "$(echo "$line" | jq -r 'has("machine_id")')" = "true" ]
|
|
328
|
+
[ "$(echo "$line" | jq -r 'has("sequence")')" = "true" ]
|
|
329
|
+
[ "$(echo "$line" | jq -r '.sequence | type')" = "number" ]
|
|
319
330
|
}
|
|
320
331
|
|
|
321
|
-
@test "emit:
|
|
332
|
+
@test "emit: honors ONLOOKER_PLUGIN_NAME for the plugin field" {
|
|
322
333
|
: >"$ONLOOKER_EVENTS_LOG"
|
|
323
|
-
|
|
324
|
-
[ "$status" -eq 0 ]
|
|
334
|
+
ONLOOKER_PLUGIN_NAME="prompt-rules" prompt_rules_emit "sess-plugin" "prompt_rule.applied" '{"rule_id":"r9"}'
|
|
325
335
|
|
|
326
336
|
local line
|
|
327
337
|
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
328
|
-
[ "$(echo "$line" | jq -
|
|
338
|
+
[ "$(echo "$line" | jq -r '.plugin')" = "prompt-rules" ]
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
@test "emit: defaults session id to 'unknown' when none is given" {
|
|
332
342
|
: >"$ONLOOKER_EVENTS_LOG"
|
|
333
|
-
prompt_rules_emit "" "prompt_rule.matched"
|
|
343
|
+
prompt_rules_emit "" "prompt_rule.matched" "$MATCHED_PAYLOAD"
|
|
334
344
|
|
|
335
345
|
local line
|
|
336
346
|
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
337
347
|
[ "$(echo "$line" | jq -r '.session_id')" = "unknown" ]
|
|
338
348
|
}
|
|
339
349
|
|
|
340
|
-
|
|
350
|
+
# The old hand-built envelope added `turn` from ONLOOKER_TURN_NUMBER. event.v1.json
|
|
351
|
+
# is additionalProperties:false and has no `turn`, so that field was one of the
|
|
352
|
+
# reasons those lines could never validate. The canonical emitter carries a turn
|
|
353
|
+
# as `turn_number` inside the payload, and neither prompt_rule payload schema
|
|
354
|
+
# declares that property, so the turn has nowhere valid to live on these events.
|
|
355
|
+
@test "emit: does not add a turn field the envelope schema forbids" {
|
|
341
356
|
: >"$ONLOOKER_EVENTS_LOG"
|
|
342
|
-
ONLOOKER_TURN_NUMBER=7 prompt_rules_emit "sess-turn" "prompt_rule.matched"
|
|
357
|
+
ONLOOKER_TURN_NUMBER=7 prompt_rules_emit "sess-turn" "prompt_rule.matched" "$MATCHED_PAYLOAD"
|
|
343
358
|
|
|
344
359
|
local line
|
|
345
360
|
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
346
|
-
[ "$(echo "$line" | jq -r '
|
|
347
|
-
[ "$(echo "$line" | jq -r '.
|
|
361
|
+
[ "$(echo "$line" | jq -r 'has("turn")')" = "false" ]
|
|
362
|
+
[ "$(echo "$line" | jq -r '.payload | has("turn_number")')" = "false" ]
|
|
348
363
|
}
|
|
349
364
|
|
|
350
|
-
@test "emit:
|
|
365
|
+
@test "emit: returns 1 and writes nothing when event_type is empty" {
|
|
351
366
|
: >"$ONLOOKER_EVENTS_LOG"
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
367
|
+
run prompt_rules_emit "sess-empty" ""
|
|
368
|
+
[ "$status" -eq 1 ]
|
|
369
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
370
|
+
}
|
|
355
371
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
372
|
+
# Fail-closed is the whole point of routing through the emitter: an event that
|
|
373
|
+
# cannot be made valid must not reach the bus at all. Before this change the
|
|
374
|
+
# same call wrote a line regardless.
|
|
375
|
+
@test "emit: rejects a matched payload missing the schema's required fields" {
|
|
376
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
377
|
+
expect_emission_rejected prompt_rules_emit "sess-bad" "prompt_rule.matched" '{"rule_id":"only-id"}'
|
|
378
|
+
[ "$status" -ne 0 ]
|
|
379
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
360
380
|
}
|
|
361
381
|
|
|
362
|
-
@test "emit:
|
|
382
|
+
@test "emit: rejects an empty payload for an event type that requires fields" {
|
|
363
383
|
: >"$ONLOOKER_EVENTS_LOG"
|
|
364
|
-
|
|
365
|
-
[ "$status" -
|
|
384
|
+
expect_emission_rejected prompt_rules_emit "sess-nopayload" "prompt_rule.matched"
|
|
385
|
+
[ "$status" -ne 0 ]
|
|
386
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@test "emit: rejects an unknown event type" {
|
|
390
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
391
|
+
expect_emission_rejected prompt_rules_emit "sess-unknown" "prompt_rule.invented" '{}'
|
|
392
|
+
[ "$status" -ne 0 ]
|
|
366
393
|
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
367
394
|
}
|
|
395
|
+
|
|
396
|
+
@test "emit: accepts prompt_rule.applied with the fields the injector sends" {
|
|
397
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
398
|
+
run prompt_rules_emit "sess-applied" "prompt_rule.applied" '{"rule_id":"r9","guidance_chars":42}'
|
|
399
|
+
[ "$status" -eq 0 ]
|
|
400
|
+
|
|
401
|
+
local line
|
|
402
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
403
|
+
[ "$(echo "$line" | jq -r '.payload.guidance_chars')" = "42" ]
|
|
404
|
+
}
|
package/test/bus-coverage.json
CHANGED
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
"lineage.query.answered",
|
|
56
56
|
"memory.recalled",
|
|
57
57
|
"onlooker.artifact.ready",
|
|
58
|
+
"prompt_rule.applied",
|
|
59
|
+
"prompt_rule.matched",
|
|
58
60
|
"session.compact",
|
|
59
61
|
"session.end",
|
|
60
62
|
"session.prompt",
|
|
@@ -113,8 +115,6 @@
|
|
|
113
115
|
"onlooker.session.summary": "emitted by the agent, not this repo",
|
|
114
116
|
"oracle.calibration.complete": "plugin lives in another repo",
|
|
115
117
|
"oracle.calibration.requested": "plugin lives in another repo",
|
|
116
|
-
"prompt_rule.applied": "prompt_rules_emit in scripts/lib/prompt-rules.sh writes straight to $ONLOOKER_EVENTS_LOG via jq, bypassing onlooker-event.mjs, so it never reaches the schema-validating emitter this harness observes even though test/bats/prompt-rules.bats exercises it",
|
|
117
|
-
"prompt_rule.matched": "prompt_rules_emit in scripts/lib/prompt-rules.sh writes straight to $ONLOOKER_EVENTS_LOG via jq, bypassing onlooker-event.mjs, so it never reaches the schema-validating emitter this harness observes even though test/bats/prompt-rules.bats exercises it",
|
|
118
118
|
"relay.handoff.captured": "plugin lives in another repo",
|
|
119
119
|
"relay.handoff.injected": "plugin lives in another repo",
|
|
120
120
|
"scribe.capture.complete": "no scribe_emit_event call for this type exists anywhere in plugins/scribe; only scribe.distill.complete and onlooker.artifact.ready are wired up",
|