@onlooker-community/ecosystem 0.31.0 → 0.31.2

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 (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.release-please-manifest.json +17 -17
  3. package/CHANGELOG.md +14 -0
  4. package/package.json +1 -1
  5. package/plugins/archivist/.claude-plugin/plugin.json +1 -1
  6. package/plugins/archivist/CHANGELOG.md +7 -0
  7. package/plugins/archivist/scripts/hooks/archivist-extract.sh +10 -0
  8. package/plugins/archivist/scripts/hooks/archivist-inject.sh +10 -0
  9. package/plugins/archivist/scripts/lib/archivist-events.sh +6 -0
  10. package/plugins/assayer/.claude-plugin/plugin.json +1 -1
  11. package/plugins/assayer/CHANGELOG.md +7 -0
  12. package/plugins/assayer/scripts/hooks/assayer-stop.sh +10 -0
  13. package/plugins/assayer/scripts/lib/assayer-events.sh +6 -0
  14. package/plugins/bursar/.claude-plugin/plugin.json +1 -1
  15. package/plugins/bursar/CHANGELOG.md +7 -0
  16. package/plugins/bursar/scripts/lib/bursar-events.sh +6 -0
  17. package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
  18. package/plugins/cartographer/CHANGELOG.md +7 -0
  19. package/plugins/cartographer/scripts/lib/cartographer-events.sh +6 -0
  20. package/plugins/compass/.claude-plugin/plugin.json +1 -1
  21. package/plugins/compass/CHANGELOG.md +7 -0
  22. package/plugins/compass/scripts/lib/compass-events.sh +6 -0
  23. package/plugins/counsel/.claude-plugin/plugin.json +1 -1
  24. package/plugins/counsel/CHANGELOG.md +7 -0
  25. package/plugins/counsel/scripts/lib/counsel-events.sh +6 -0
  26. package/plugins/curator/.claude-plugin/plugin.json +1 -1
  27. package/plugins/curator/CHANGELOG.md +7 -0
  28. package/plugins/curator/scripts/hooks/curator-session-start.sh +10 -0
  29. package/plugins/curator/scripts/lib/curator-emit.sh +10 -0
  30. package/plugins/echo/.claude-plugin/plugin.json +1 -1
  31. package/plugins/echo/CHANGELOG.md +7 -0
  32. package/plugins/echo/scripts/hooks/echo-stop-gate.sh +10 -0
  33. package/plugins/echo/scripts/lib/echo-events.sh +6 -0
  34. package/plugins/governor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/governor/CHANGELOG.md +7 -0
  36. package/plugins/governor/scripts/hooks/governor-post-tool-use.sh +10 -0
  37. package/plugins/governor/scripts/hooks/governor-pre-tool-use.sh +10 -0
  38. package/plugins/governor/scripts/hooks/governor-session-start.sh +10 -0
  39. package/plugins/governor/scripts/hooks/governor-stop.sh +10 -0
  40. package/plugins/governor/scripts/lib/governor-events.sh +6 -0
  41. package/plugins/historian/.claude-plugin/plugin.json +1 -1
  42. package/plugins/historian/CHANGELOG.md +14 -0
  43. package/plugins/historian/scripts/hooks/historian-prompt-submit.sh +12 -1
  44. package/plugins/historian/scripts/hooks/historian-session-end.sh +12 -1
  45. package/plugins/historian/scripts/lib/historian-emit.sh +10 -0
  46. package/plugins/inspector/.claude-plugin/plugin.json +1 -1
  47. package/plugins/inspector/CHANGELOG.md +7 -0
  48. package/plugins/inspector/scripts/lib/inspector-events.sh +6 -0
  49. package/plugins/librarian/.claude-plugin/plugin.json +1 -1
  50. package/plugins/librarian/CHANGELOG.md +7 -0
  51. package/plugins/librarian/scripts/hooks/librarian-session-end.sh +80 -6
  52. package/plugins/librarian/scripts/hooks/librarian-session-start.sh +10 -0
  53. package/plugins/librarian/scripts/lib/librarian-conflict-detector.sh +215 -0
  54. package/plugins/librarian/scripts/lib/librarian-emit.sh +10 -0
  55. package/plugins/lineage/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lineage/CHANGELOG.md +7 -0
  57. package/plugins/lineage/scripts/lib/lineage-events.sh +6 -0
  58. package/plugins/scribe/.claude-plugin/plugin.json +1 -1
  59. package/plugins/scribe/CHANGELOG.md +7 -0
  60. package/plugins/scribe/scripts/lib/scribe-events.sh +6 -0
  61. package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
  62. package/plugins/tribunal/CHANGELOG.md +7 -0
  63. package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +10 -0
  64. package/plugins/tribunal/scripts/lib/tribunal-events.sh +6 -0
  65. package/plugins/warden/.claude-plugin/plugin.json +1 -1
  66. package/plugins/warden/CHANGELOG.md +7 -0
  67. package/plugins/warden/scripts/lib/warden-events.sh +6 -0
@@ -25,9 +25,20 @@ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
25
25
  _ECOSYSTEM_ROOT="$_candidate"
26
26
  fi
27
27
  fi
28
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
29
+ # works regardless of which ecosystem version is installed.
30
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
31
+ for _candidate in "${PLUGIN_ROOT}/../../ecosystem/"*/scripts/lib/validate-path.sh; do
32
+ if [[ -f "$_candidate" ]]; then
33
+ _ECOSYSTEM_ROOT="$(cd "$(dirname "$(dirname "$_candidate")")" && pwd)"
34
+ break
35
+ fi
36
+ done
37
+ fi
28
38
  if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
29
39
  # shellcheck disable=SC1091
30
- CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
40
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
41
+ source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
31
42
  fi
32
43
 
33
44
  # shellcheck source=../lib/historian-config.sh
@@ -16,6 +16,16 @@ _historian_resolve_event_js() {
16
16
  ecosystem_root="$candidate"
17
17
  fi
18
18
  fi
19
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
20
+ # works regardless of which ecosystem version is installed.
21
+ if [[ -z "$ecosystem_root" ]]; then
22
+ for candidate in "${plugin_root}/../../ecosystem/"*/scripts/lib/onlooker-event.mjs; do
23
+ if [[ -f "$candidate" ]]; then
24
+ ecosystem_root="$(cd "$(dirname "$(dirname "$candidate")")" && pwd)"
25
+ break
26
+ fi
27
+ done
28
+ fi
19
29
 
20
30
  if [[ -n "$ecosystem_root" ]]; then
21
31
  printf '%s/scripts/lib/onlooker-event.mjs' "$ecosystem_root"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inspector",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
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
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/onlooker-community/ecosystem/compare/inspector-v0.3.0...inspector-v0.3.1) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **plugins:** glob-discover ecosystem root so sub-plugins work from cache :relieved: ([#110](https://github.com/onlooker-community/ecosystem/issues/110)) ([3639240](https://github.com/onlooker-community/ecosystem/commit/3639240383e1b820e5d8ea42639e0b863ef0d90e))
9
+
3
10
  ## [0.3.0](https://github.com/onlooker-community/ecosystem/compare/inspector-v0.2.0...inspector-v0.3.0) (2026-07-03)
4
11
 
5
12
 
@@ -23,6 +23,12 @@ _inspector_event_js_path() {
23
23
  for c in "${candidates[@]}"; do
24
24
  [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
25
25
  done
26
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
27
+ # works regardless of which ecosystem version is installed.
28
+ local mjs
29
+ for mjs in "${plugin_root}/../../ecosystem/"*/scripts/lib/onlooker-event.mjs; do
30
+ [[ -f "$mjs" ]] && { printf '%s' "$mjs"; return 0; }
31
+ done
26
32
  return 1
27
33
  }
28
34
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librarian",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Consolidation layer between archivist's per-session artifacts and the user's durable typed memory store. Detects which session decisions, dead-ends, and open questions deserve to live across sessions, classifies them into the user/feedback/project/reference types, and queues them as proposals for explicit confirmation. Auto-promotion is opt-in. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.4.0...librarian-v0.4.1) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **plugins:** glob-discover ecosystem root so sub-plugins work from cache :relieved: ([#110](https://github.com/onlooker-community/ecosystem/issues/110)) ([3639240](https://github.com/onlooker-community/ecosystem/commit/3639240383e1b820e5d8ea42639e0b863ef0d90e))
9
+
3
10
  ## [0.4.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.3.0...librarian-v0.4.0) (2026-07-03)
4
11
 
5
12
 
@@ -25,6 +25,16 @@ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
25
25
  _ECOSYSTEM_ROOT="$_candidate"
26
26
  fi
27
27
  fi
28
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
29
+ # works regardless of which ecosystem version is installed.
30
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
31
+ for _candidate in "${PLUGIN_ROOT}/../../ecosystem/"*/scripts/lib/validate-path.sh; do
32
+ if [[ -f "$_candidate" ]]; then
33
+ _ECOSYSTEM_ROOT="$(cd "$(dirname "$(dirname "$_candidate")")" && pwd)"
34
+ break
35
+ fi
36
+ done
37
+ fi
28
38
 
29
39
  if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
30
40
  # shellcheck disable=SC1091
@@ -47,6 +57,8 @@ source "${PLUGIN_ROOT}/scripts/lib/librarian-archivist-reader.sh"
47
57
  source "${PLUGIN_ROOT}/scripts/lib/librarian-durability.sh"
48
58
  # shellcheck source=../lib/librarian-classifier.sh
49
59
  source "${PLUGIN_ROOT}/scripts/lib/librarian-classifier.sh"
60
+ # shellcheck source=../lib/librarian-conflict-detector.sh
61
+ source "${PLUGIN_ROOT}/scripts/lib/librarian-conflict-detector.sh"
50
62
 
51
63
  INPUT=$(cat 2>/dev/null || true)
52
64
  CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
@@ -222,14 +234,75 @@ for ((i = 0; i < KEPT_COUNT; i++)); do
222
234
  continue
223
235
  fi
224
236
 
225
- # Build and persist the proposal. Conflict detection against the user's
226
- # memory store is deferred to a follow-up commit; everything ships as
227
- # conflict_state: "none" for now.
237
+ # Build and persist the proposal. Detect conflicts against the user's
238
+ # memory store before writing.
228
239
  PROPOSAL_ID=$(librarian_ulid)
229
240
  FILENAME=$(librarian_classifier_filename "$MEMORY_TYPE" "$TITLE")
230
241
  ARTIFACT_ID=$(printf '%s' "$ARTIFACT" | jq -r '.id // ""')
231
242
  ARTIFACT_SESSION=$(printf '%s' "$ARTIFACT" | jq -r '.session_id // ""')
232
243
 
244
+ # Resolve the typed memory store path for this project.
245
+ MEMORY_STORE_PATH=$(librarian_config_get '.librarian.memory_store_path')
246
+ [[ -z "$MEMORY_STORE_PATH" || "$MEMORY_STORE_PATH" == "null" ]] && \
247
+ MEMORY_STORE_PATH='${HOME}/.claude/projects/${CLAUDE_PROJECT_ENCODED}/memory'
248
+
249
+ # Expand env vars in the path.
250
+ MEMORY_STORE_PATH=$(eval echo "$MEMORY_STORE_PATH")
251
+
252
+ # Initialize conflict state; will scan if memory dir exists.
253
+ CONFLICT_STATE="none"
254
+ CONFLICT_WITH="[]"
255
+
256
+ # Detect conflicts against existing memories only if the directory exists.
257
+ if [[ -d "$MEMORY_STORE_PATH" ]]; then
258
+ # Build a temporary proposal for conflict detection.
259
+ TEMP_PROPOSAL=$(jq -n \
260
+ --arg id "$PROPOSAL_ID" \
261
+ --arg memory_type "$MEMORY_TYPE" \
262
+ --arg filename "$FILENAME" \
263
+ --arg title "$TITLE" \
264
+ --arg body "$BODY" \
265
+ --argjson classifier_confidence "$CONFIDENCE" \
266
+ '{
267
+ id: $id,
268
+ proposed: {
269
+ type: $memory_type,
270
+ filename: $filename,
271
+ title: $title,
272
+ body: $body,
273
+ classifier_confidence: $classifier_confidence
274
+ }
275
+ }')
276
+
277
+ # Detect conflicts against existing memories.
278
+ DUP_THRESHOLD=$(librarian_config_get '.librarian.conflict.duplicate_threshold')
279
+ [[ -z "$DUP_THRESHOLD" || "$DUP_THRESHOLD" == "null" ]] && DUP_THRESHOLD="0.7"
280
+ MERGE_THRESHOLD=$(librarian_config_get '.librarian.conflict.merge_candidate_threshold')
281
+ [[ -z "$MERGE_THRESHOLD" || "$MERGE_THRESHOLD" == "null" ]] && MERGE_THRESHOLD="0.45"
282
+
283
+ CONFLICT_RESULT=$(librarian_conflict_scan "$TEMP_PROPOSAL" "$MEMORY_STORE_PATH" \
284
+ "$DUP_THRESHOLD" "$MERGE_THRESHOLD" 2>/dev/null) || CONFLICT_RESULT=""
285
+
286
+ # Ensure we have valid JSON; fall back to "none" if scan fails.
287
+ if [[ -z "$CONFLICT_RESULT" ]] || ! printf '%s' "$CONFLICT_RESULT" | jq -e '.' >/dev/null 2>&1; then
288
+ CONFLICT_RESULT='{"conflict_state":"none","conflict_with":[]}'
289
+ fi
290
+
291
+ CONFLICT_STATE=$(printf '%s' "$CONFLICT_RESULT" | jq -r '.conflict_state // "none"' 2>/dev/null) || CONFLICT_STATE="none"
292
+ CONFLICT_WITH=$(printf '%s' "$CONFLICT_RESULT" | jq -c '.conflict_with // []' 2>/dev/null) || CONFLICT_WITH="[]"
293
+
294
+ # Silently drop duplicates — no proposal written.
295
+ if [[ "$CONFLICT_STATE" == "duplicate" ]]; then
296
+ POST_CLASSIFIER_DROPPED=$((POST_CLASSIFIER_DROPPED + 1))
297
+ librarian_emit "librarian.candidate.dropped" "$SESSION_ID" "$(jq -cn \
298
+ --arg reason "duplicate" \
299
+ --arg src "$ARTIFACT_ID" \
300
+ '{ reason: $reason, source_artifact_id: (if $src == "" then null else $src end) }
301
+ | with_entries(select(.value != null))')"
302
+ continue
303
+ fi
304
+ fi
305
+
233
306
  PROPOSAL_JSON=$(jq -n \
234
307
  --arg id "$PROPOSAL_ID" \
235
308
  --arg created_at "$NOW_TS" \
@@ -238,7 +311,8 @@ for ((i = 0; i < KEPT_COUNT; i++)); do
238
311
  --arg title "$TITLE" \
239
312
  --arg body "$BODY" \
240
313
  --argjson classifier_confidence "$CONFIDENCE" \
241
- --arg conflict_state "none" \
314
+ --arg conflict_state "$CONFLICT_STATE" \
315
+ --argjson conflict_with "$CONFLICT_WITH" \
242
316
  --arg artifact_id "$ARTIFACT_ID" \
243
317
  --arg artifact_session "$ARTIFACT_SESSION" \
244
318
  '{
@@ -254,7 +328,7 @@ for ((i = 0; i < KEPT_COUNT; i++)); do
254
328
  classifier_confidence: $classifier_confidence
255
329
  },
256
330
  conflict_state: $conflict_state,
257
- conflict_with: [],
331
+ conflict_with: $conflict_with,
258
332
  status: "pending"
259
333
  }')
260
334
 
@@ -304,7 +378,7 @@ for ((i = 0; i < KEPT_COUNT; i++)); do
304
378
  --arg proposal_id "$PROPOSAL_ID" \
305
379
  --arg memory_type "$MEMORY_TYPE" \
306
380
  --argjson classifier_confidence "$CONFIDENCE" \
307
- --arg conflict_state "none" \
381
+ --arg conflict_state "$CONFLICT_STATE" \
308
382
  --arg src "$ARTIFACT_ID" \
309
383
  '{
310
384
  proposal_id: $proposal_id,
@@ -25,6 +25,16 @@ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
25
25
  _ECOSYSTEM_ROOT="$_candidate"
26
26
  fi
27
27
  fi
28
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
29
+ # works regardless of which ecosystem version is installed.
30
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
31
+ for _candidate in "${PLUGIN_ROOT}/../../ecosystem/"*/scripts/lib/validate-path.sh; do
32
+ if [[ -f "$_candidate" ]]; then
33
+ _ECOSYSTEM_ROOT="$(cd "$(dirname "$(dirname "$_candidate")")" && pwd)"
34
+ break
35
+ fi
36
+ done
37
+ fi
28
38
  if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
29
39
  # shellcheck disable=SC1091
30
40
  CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env bash
2
+ # Conflict and duplicate detection for librarian proposals.
3
+ #
4
+ # Compares proposed memories against existing memories in the user's
5
+ # typed memory store (~/.claude/projects/<encoded>/memory/) and classifies
6
+ # each proposal into one of:
7
+ # - none (no similarity, safe to propose)
8
+ # - duplicate (similarity >= duplicate_threshold; drop silently)
9
+ # - merge_candidate (merge_threshold <= similarity < duplicate_threshold)
10
+ # - conflict_candidate (opposing sentiment on overlapping content)
11
+ #
12
+ # Uses deterministic token-set Jaccard similarity (no LLM call) with
13
+ # configurable thresholds.
14
+
15
+ # Extract and normalize tokens from text for similarity comparison.
16
+ # Lowercases, removes stopwords, splits on non-alphanumeric.
17
+ _librarian_conflict_normalize() {
18
+ local text="$1"
19
+ printf '%s' "$text" \
20
+ | tr '[:upper:]' '[:lower:]' \
21
+ | tr -cs '[:alnum:]' '\n' \
22
+ | grep -v '^$' \
23
+ | sort -u \
24
+ | paste -sd ' ' -
25
+ }
26
+
27
+ # Compute Jaccard similarity (intersection / union) of two token sets.
28
+ # Input: two space-separated token strings.
29
+ # Output: float 0-1.
30
+ _librarian_conflict_jaccard() {
31
+ local tokens1="$1"
32
+ local tokens2="$2"
33
+
34
+ [[ -z "$tokens1" || -z "$tokens2" ]] && { printf '0'; return; }
35
+
36
+ TOKENS1="$tokens1" TOKENS2="$tokens2" python3 -c '
37
+ import os
38
+ tokens1 = set(os.environ.get("TOKENS1", "").split())
39
+ tokens2 = set(os.environ.get("TOKENS2", "").split())
40
+ if not tokens1 and not tokens2:
41
+ print(0)
42
+ else:
43
+ intersection = len(tokens1 & tokens2)
44
+ union = len(tokens1 | tokens2)
45
+ similarity = intersection / union if union > 0 else 0
46
+ print(f"{similarity:.2f}")
47
+ '
48
+ }
49
+
50
+ # Check for opposing sentiment markers (do/don't, always/never, etc.).
51
+ # Returns 0 if opposing markers found, 1 otherwise.
52
+ _librarian_conflict_opposing_sentiment() {
53
+ local text1="$1"
54
+ local text2="$2"
55
+
56
+ # Normalize to lowercase.
57
+ text1=$(printf '%s' "$text1" | tr '[:upper:]' '[:lower:]')
58
+ text2=$(printf '%s' "$text2" | tr '[:upper:]' '[:lower:]')
59
+
60
+ # Define opposing pairs.
61
+ local -a pairs=("always:never" "never:always" "do:don't" "don't:do" "prefer:avoid"
62
+ "avoid:prefer" "enable:disable" "disable:enable" "keep:remove"
63
+ "remove:keep" "use:avoid" "avoid:use")
64
+
65
+ for pair in "${pairs[@]}"; do
66
+ local a="${pair%%:*}"
67
+ local b="${pair##*:}"
68
+
69
+ # Check if text1 has 'a' and text2 has 'b', or vice versa.
70
+ if [[ "$text1" == *"$a"* && "$text2" == *"$b"* ]]; then
71
+ return 0 # opposing
72
+ fi
73
+ if [[ "$text1" == *"$b"* && "$text2" == *"$a"* ]]; then
74
+ return 0 # opposing
75
+ fi
76
+ done
77
+
78
+ return 1 # not opposing
79
+ }
80
+
81
+ # Detect conflict state for a single proposal against a single existing memory.
82
+ # Returns conflict classification and (optionally) the conflicting memory filename.
83
+ #
84
+ # Usage: librarian_conflict_check <proposal_body> <existing_memory_path> \
85
+ # <duplicate_threshold> <merge_threshold>
86
+ #
87
+ # Output: JSON object { conflict_state, conflicting_file (optional) }
88
+ librarian_conflict_check() {
89
+ local proposal_body="$1"
90
+ local memory_path="$2"
91
+ local dup_threshold="${3:-0.7}"
92
+ local merge_threshold="${4:-0.45}"
93
+
94
+ [[ -z "$proposal_body" || -z "$memory_path" ]] && {
95
+ printf '%s' '{"conflict_state":"none"}'
96
+ return 0
97
+ }
98
+ [[ ! -f "$memory_path" ]] && {
99
+ printf '%s' '{"conflict_state":"none"}'
100
+ return 0
101
+ }
102
+
103
+ # Extract the body of the existing memory (skip frontmatter).
104
+ local existing_body
105
+ existing_body=$(awk '/^---$/{ if (++count == 2) { in_body = 1; next } }
106
+ in_body { print }' "$memory_path")
107
+
108
+ [[ -z "$existing_body" ]] && {
109
+ printf '%s' '{"conflict_state":"none"}'
110
+ return 0
111
+ }
112
+
113
+ # Normalize both bodies to token sets.
114
+ local prop_tokens existing_tokens
115
+ prop_tokens=$(_librarian_conflict_normalize "$proposal_body")
116
+ existing_tokens=$(_librarian_conflict_normalize "$existing_body")
117
+
118
+ # Compute Jaccard similarity.
119
+ local similarity
120
+ similarity=$(_librarian_conflict_jaccard "$prop_tokens" "$existing_tokens")
121
+
122
+ # Classify based on similarity + optional sentiment check.
123
+ if awk "BEGIN { exit !($similarity >= $dup_threshold) }"; then
124
+ # Definitely a duplicate.
125
+ jq -cn --arg file "$(basename "$memory_path")" \
126
+ '{ conflict_state: "duplicate", conflicting_file: $file }'
127
+ elif awk "BEGIN { exit !($similarity >= $merge_threshold) }"; then
128
+ # Merge candidate — significant overlap but not identical.
129
+ jq -cn --arg file "$(basename "$memory_path")" \
130
+ '{ conflict_state: "merge_candidate", conflicting_file: $file }'
131
+ elif awk "BEGIN { exit !($similarity >= $merge_threshold * 0.8) }"; then
132
+ # Check for opposing sentiment in the overlap zone.
133
+ if _librarian_conflict_opposing_sentiment "$proposal_body" "$existing_body"; then
134
+ jq -cn --arg file "$(basename "$memory_path")" \
135
+ '{ conflict_state: "conflict_candidate", conflicting_file: $file }'
136
+ else
137
+ printf '%s' '{"conflict_state":"none"}'
138
+ fi
139
+ else
140
+ # No meaningful overlap.
141
+ printf '%s' '{"conflict_state":"none"}'
142
+ fi
143
+ }
144
+
145
+ # Scan a proposal against all existing memories in the project.
146
+ # Returns the highest-priority conflict found (duplicate > conflict > merge).
147
+ #
148
+ # Usage: librarian_conflict_scan <proposal_json> <memory_dir> \
149
+ # <duplicate_threshold> <merge_threshold>
150
+ #
151
+ # Output: JSON object { conflict_state, conflict_with: [...] }
152
+ librarian_conflict_scan() {
153
+ local proposal="$1"
154
+ local memory_dir="$2"
155
+ local dup_threshold="${3:-0.7}"
156
+ local merge_threshold="${4:-0.45}"
157
+
158
+ [[ -z "$proposal" || -z "$memory_dir" ]] && {
159
+ printf '%s' '{"conflict_state":"none","conflict_with":[]}'
160
+ return 0
161
+ }
162
+ [[ ! -d "$memory_dir" ]] && {
163
+ printf '%s' '{"conflict_state":"none","conflict_with":[]}'
164
+ return 0
165
+ }
166
+
167
+ local proposal_body
168
+ proposal_body=$(printf '%s' "$proposal" | jq -r '.proposed.body // ""')
169
+ [[ -z "$proposal_body" ]] && {
170
+ printf '%s' '{"conflict_state":"none","conflict_with":[]}'
171
+ return 0
172
+ }
173
+
174
+ local worst_state=""
175
+ local conflicts='[]'
176
+
177
+ # Scan all memory files in the project.
178
+ for memory_file in "$memory_dir"/*.md; do
179
+ [[ ! -f "$memory_file" ]] && continue
180
+
181
+ local check
182
+ check=$(librarian_conflict_check "$proposal_body" "$memory_file" \
183
+ "$dup_threshold" "$merge_threshold")
184
+ [[ -z "$check" ]] && continue
185
+
186
+ local state
187
+ state=$(printf '%s' "$check" | jq -r '.conflict_state // "none"')
188
+ local filename
189
+ filename=$(printf '%s' "$check" | jq -r '.conflicting_file // ""')
190
+
191
+ # Track conflicts; prioritize by severity: duplicate > conflict > merge.
192
+ case "$state" in
193
+ duplicate)
194
+ worst_state="duplicate"
195
+ conflicts=$(printf '%s' "$conflicts" | jq --arg f "$filename" '. + [$f]')
196
+ ;;
197
+ conflict_candidate)
198
+ if [[ "$worst_state" != "duplicate" ]]; then
199
+ worst_state="conflict_candidate"
200
+ conflicts=$(printf '%s' "$conflicts" | jq --arg f "$filename" '. + [$f]')
201
+ fi
202
+ ;;
203
+ merge_candidate)
204
+ if [[ -z "$worst_state" ]]; then
205
+ worst_state="merge_candidate"
206
+ conflicts=$(printf '%s' "$conflicts" | jq --arg f "$filename" '. + [$f]')
207
+ fi
208
+ ;;
209
+ esac
210
+ done
211
+
212
+ [[ -z "$worst_state" ]] && worst_state="none"
213
+ printf '%s' "$(jq -cn --arg state "$worst_state" --argjson files "$conflicts" \
214
+ '{ conflict_state: $state, conflict_with: $files }')"
215
+ }
@@ -21,6 +21,16 @@ _librarian_resolve_event_js() {
21
21
  ecosystem_root="$candidate"
22
22
  fi
23
23
  fi
24
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
25
+ # works regardless of which ecosystem version is installed.
26
+ if [[ -z "$ecosystem_root" ]]; then
27
+ for candidate in "${plugin_root}/../../ecosystem/"*/scripts/lib/onlooker-event.mjs; do
28
+ if [[ -f "$candidate" ]]; then
29
+ ecosystem_root="$(cd "$(dirname "$(dirname "$candidate")")" && pwd)"
30
+ break
31
+ fi
32
+ done
33
+ fi
24
34
 
25
35
  if [[ -n "$ecosystem_root" ]]; then
26
36
  printf '%s/scripts/lib/onlooker-event.mjs' "$ecosystem_root"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lineage",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Per-change provenance for the Onlooker ecosystem. Records session/turn metadata plus a secret-redacted, size-capped snippet for every Edit/Write/MultiEdit at PostToolUse, then answers \"why does this line exist?\" via /lineage by joining change records to historian-preserved transcripts. Emits lineage.* events for audit. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.1](https://github.com/onlooker-community/ecosystem/compare/lineage-v0.2.0...lineage-v0.2.1) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **plugins:** glob-discover ecosystem root so sub-plugins work from cache :relieved: ([#110](https://github.com/onlooker-community/ecosystem/issues/110)) ([3639240](https://github.com/onlooker-community/ecosystem/commit/3639240383e1b820e5d8ea42639e0b863ef0d90e))
9
+
3
10
  ## [0.2.0](https://github.com/onlooker-community/ecosystem/compare/lineage-v0.1.0...lineage-v0.2.0) (2026-07-03)
4
11
 
5
12
 
@@ -24,6 +24,12 @@ _lineage_event_js_path() {
24
24
  for c in "${candidates[@]}"; do
25
25
  [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
26
26
  done
27
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
28
+ # works regardless of which ecosystem version is installed.
29
+ local mjs
30
+ for mjs in "${plugin_root}/../../ecosystem/"*/scripts/lib/onlooker-event.mjs; do
31
+ [[ -f "$mjs" ]] && { printf '%s' "$mjs"; return 0; }
32
+ done
27
33
  return 1
28
34
  }
29
35
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scribe",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Intent documentation from agent activity. Captures why changes were made — problem context, decisions, tradeoffs — and distills them into readable artifacts at session end.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1](https://github.com/onlooker-community/ecosystem/compare/scribe-v0.4.0...scribe-v0.4.1) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **plugins:** glob-discover ecosystem root so sub-plugins work from cache :relieved: ([#110](https://github.com/onlooker-community/ecosystem/issues/110)) ([3639240](https://github.com/onlooker-community/ecosystem/commit/3639240383e1b820e5d8ea42639e0b863ef0d90e))
9
+
3
10
  ## [0.4.0](https://github.com/onlooker-community/ecosystem/compare/scribe-v0.3.0...scribe-v0.4.0) (2026-07-03)
4
11
 
5
12
 
@@ -24,6 +24,12 @@ _scribe_event_js_path() {
24
24
  for c in "${candidates[@]}"; do
25
25
  [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
26
26
  done
27
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
28
+ # works regardless of which ecosystem version is installed.
29
+ local mjs
30
+ for mjs in "${plugin_root}/../../ecosystem/"*/scripts/lib/onlooker-event.mjs; do
31
+ [[ -f "$mjs" ]] && { printf '%s' "$mjs"; return 0; }
32
+ done
27
33
  return 1
28
34
  }
29
35
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tribunal",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Multi-agent execution with LLM-as-a-Judge quality gates. An Actor performs work; a jury of typed Judges scores it against a project-overridable rubric; a Meta-Judge reviews the jury for bias; the gate decides accept, retry, or exhaust. Grounded in LLM-as-a-Judge (Zheng et al. 2023) and LLM-as-a-Meta-Judge (Wu et al. 2024). Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.1](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.1.0...tribunal-v1.1.1) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **plugins:** glob-discover ecosystem root so sub-plugins work from cache :relieved: ([#110](https://github.com/onlooker-community/ecosystem/issues/110)) ([3639240](https://github.com/onlooker-community/ecosystem/commit/3639240383e1b820e5d8ea42639e0b863ef0d90e))
9
+
3
10
  ## [1.1.0](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.0.1...tribunal-v1.1.0) (2026-07-03)
4
11
 
5
12
 
@@ -24,6 +24,16 @@ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
24
24
  _ECOSYSTEM_ROOT="$_candidate"
25
25
  fi
26
26
  fi
27
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
28
+ # works regardless of which ecosystem version is installed.
29
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
30
+ for _candidate in "${PLUGIN_ROOT}/../../ecosystem/"*/scripts/lib/validate-path.sh; do
31
+ if [[ -f "$_candidate" ]]; then
32
+ _ECOSYSTEM_ROOT="$(cd "$(dirname "$(dirname "$_candidate")")" && pwd)"
33
+ break
34
+ fi
35
+ done
36
+ fi
27
37
 
28
38
  if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
29
39
  # shellcheck disable=SC1091
@@ -37,6 +37,12 @@ _tribunal_event_js_path() {
37
37
  for c in "${candidates[@]}"; do
38
38
  [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
39
39
  done
40
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
41
+ # works regardless of which ecosystem version is installed.
42
+ local mjs
43
+ for mjs in "${plugin_root}/../../ecosystem/"*/scripts/lib/onlooker-event.mjs; do
44
+ [[ -f "$mjs" ]] && { printf '%s' "$mjs"; return 0; }
45
+ done
40
46
  return 1
41
47
  }
42
48
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warden",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Untrusted-content gate. Scans content flowing in through WebFetch and Read for prompt-injection patterns, and when a threat is detected closes a session-scoped gate that blocks Write, Edit, and Bash until the user explicitly clears it. Grounded in Meta's Agents Rule of Two: an agent should hold no more than two of {private data, external actions, untrusted content} at once — warden removes the external-actions property while untrusted content is in play. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/onlooker-community/ecosystem/compare/warden-v0.3.0...warden-v0.3.1) (2026-08-01)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **plugins:** glob-discover ecosystem root so sub-plugins work from cache :relieved: ([#110](https://github.com/onlooker-community/ecosystem/issues/110)) ([3639240](https://github.com/onlooker-community/ecosystem/commit/3639240383e1b820e5d8ea42639e0b863ef0d90e))
9
+
3
10
  ## [0.3.0](https://github.com/onlooker-community/ecosystem/compare/warden-v0.2.0...warden-v0.3.0) (2026-07-03)
4
11
 
5
12
 
@@ -28,6 +28,12 @@ _warden_event_js_path() {
28
28
  for c in "${candidates[@]}"; do
29
29
  [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
30
30
  done
31
+ # Glob-discover the ecosystem plugin under the shared plugin cache parent;
32
+ # works regardless of which ecosystem version is installed.
33
+ local mjs
34
+ for mjs in "${plugin_root}/../../ecosystem/"*/scripts/lib/onlooker-event.mjs; do
35
+ [[ -f "$mjs" ]] && { printf '%s' "$mjs"; return 0; }
36
+ done
31
37
  return 1
32
38
  }
33
39