@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecosystem",
3
- "version": "0.43.3",
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",
@@ -1,5 +1,5 @@
1
1
  {
2
- ".": "0.43.3",
2
+ ".": "0.43.4",
3
3
  "plugins/archivist": "0.3.1",
4
4
  "plugins/tribunal": "1.2.7",
5
5
  "plugins/echo": "0.3.1",
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
 
@@ -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:** Not all events in the JSONL log are schema-validated. `prompt_rule.*` events are currently emitted outside the canonical schema pipeline (`prompt_rules_emit` in `scripts/lib/prompt-rules.sh` writes straight to `$ONLOOKER_EVENTS_LOG` via `jq`, bypassing `onlooker-event.mjs` entirely). Schema-first emission is the goal for all future event types.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.43.3",
3
+ "version": "0.43.4",
4
4
  "description": "Agents, skills, hooks, commands, rules, and MCP configurations that power [Onlooker](https://onlooker.dev)",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -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-ish events to ~/.onlooker/logs/onlooker-events.jsonl:
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" '{rule_id: $id}')" || true
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
- # These event types (prompt_rule.matched, prompt_rule.applied) are not yet
146
- # in @onlooker-community/schema; once added, swap to onlooker_append_event.
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 timestamp plugin
161
- timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
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
- jq -cn \
165
- --arg ts "$timestamp" \
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
- --arg turn "${ONLOOKER_TURN_NUMBER:-}" \
171
- '{timestamp: $ts, session_id: $sid, plugin: $plugin, event_type: $type, payload: $payload}
172
- + (if $turn != "" then {turn: ($turn | tonumber)} else {} end)
173
- ' >> "$ONLOOKER_EVENTS_LOG"
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" '{"rule_id":"rule-1"}'
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: honors ONLOOKER_PLUGIN_NAME for the plugin field" {
316
+ @test "emit: writes a full canonical envelope, not a hand-built subset" {
313
317
  : >"$ONLOOKER_EVENTS_LOG"
314
- ONLOOKER_PLUGIN_NAME="prompt-rules" prompt_rules_emit "sess-plugin" "prompt_rule.applied" '{"rule_id":"r9"}'
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
- [ "$(echo "$line" | jq -r '.plugin')" = "prompt-rules" ]
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: defaults payload to an empty object when none is given" {
332
+ @test "emit: honors ONLOOKER_PLUGIN_NAME for the plugin field" {
322
333
  : >"$ONLOOKER_EVENTS_LOG"
323
- run prompt_rules_emit "sess-nopayload" "prompt_rule.matched"
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 -c '.payload')" = "{}" ]
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
- @test "emit: includes a numeric turn field when ONLOOKER_TURN_NUMBER is exported" {
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 '.turn')" = "7" ]
347
- [ "$(echo "$line" | jq -r '.turn | type')" = "number" ]
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: omits the turn field when ONLOOKER_TURN_NUMBER is unset" {
365
+ @test "emit: returns 1 and writes nothing when event_type is empty" {
351
366
  : >"$ONLOOKER_EVENTS_LOG"
352
- # Guard against any ambient value leaking in from the environment.
353
- unset ONLOOKER_TURN_NUMBER
354
- prompt_rules_emit "sess-noturn" "prompt_rule.matched" '{}'
367
+ run prompt_rules_emit "sess-empty" ""
368
+ [ "$status" -eq 1 ]
369
+ [ ! -s "$ONLOOKER_EVENTS_LOG" ]
370
+ }
355
371
 
356
- local line
357
- line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
358
- [ "$(echo "$line" | jq -e 'has("turn")')" = "false" ] || \
359
- [ "$(echo "$line" | jq 'has("turn")')" = "false" ]
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: returns 1 and writes nothing when event_type is empty" {
382
+ @test "emit: rejects an empty payload for an event type that requires fields" {
363
383
  : >"$ONLOOKER_EVENTS_LOG"
364
- run prompt_rules_emit "sess-empty" ""
365
- [ "$status" -eq 1 ]
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
+ }
@@ -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",