@onlooker-community/ecosystem 0.16.0 → 0.18.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 (54) hide show
  1. package/.claude-plugin/marketplace.json +39 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.release-please-manifest.json +5 -2
  4. package/CHANGELOG.md +15 -0
  5. package/CLAUDE.md +88 -0
  6. package/package.json +2 -2
  7. package/plugins/compass/.claude-plugin/plugin.json +14 -0
  8. package/plugins/compass/CHANGELOG.md +8 -0
  9. package/plugins/compass/config.json +71 -0
  10. package/plugins/compass/docs/adr/001-evaluate-prompts-in-context.md +82 -0
  11. package/plugins/compass/docs/design.md +421 -0
  12. package/plugins/compass/hooks/hooks.json +82 -0
  13. package/plugins/compass/scripts/hooks/compass-bash-gate.sh +95 -0
  14. package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +86 -0
  15. package/plugins/compass/scripts/hooks/compass-record-write.sh +97 -0
  16. package/plugins/compass/scripts/hooks/compass-session-start.sh +77 -0
  17. package/plugins/compass/scripts/lib/compass-config.sh +72 -0
  18. package/plugins/compass/scripts/lib/compass-evaluator.sh +374 -0
  19. package/plugins/compass/scripts/lib/compass-events.sh +81 -0
  20. package/plugins/compass/scripts/lib/compass-gate.sh +465 -0
  21. package/plugins/compass/scripts/lib/compass-sanitizer.sh +82 -0
  22. package/plugins/compass/scripts/lib/compass-transcript.sh +135 -0
  23. package/plugins/counsel/.claude-plugin/plugin.json +14 -0
  24. package/plugins/counsel/CHANGELOG.md +8 -0
  25. package/plugins/counsel/config.json +20 -0
  26. package/plugins/counsel/hooks/hooks.json +15 -0
  27. package/plugins/counsel/scripts/hooks/counsel-session-start.sh +106 -0
  28. package/plugins/counsel/scripts/lib/counsel-brief.sh +247 -0
  29. package/plugins/counsel/scripts/lib/counsel-config.sh +72 -0
  30. package/plugins/counsel/scripts/lib/counsel-events.sh +80 -0
  31. package/plugins/counsel/scripts/lib/counsel-project-key.sh +79 -0
  32. package/plugins/counsel/scripts/lib/counsel-reader.sh +114 -0
  33. package/plugins/counsel/scripts/lib/counsel-synthesize.sh +103 -0
  34. package/plugins/counsel/scripts/lib/counsel-ulid.sh +45 -0
  35. package/plugins/governor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/governor/CHANGELOG.md +7 -0
  37. package/plugins/scribe/.claude-plugin/plugin.json +12 -0
  38. package/plugins/scribe/CHANGELOG.md +8 -0
  39. package/plugins/scribe/config.json +20 -0
  40. package/plugins/scribe/hooks/hooks.json +37 -0
  41. package/plugins/scribe/scripts/hooks/scribe-capture.sh +76 -0
  42. package/plugins/scribe/scripts/hooks/scribe-session-start.sh +58 -0
  43. package/plugins/scribe/scripts/hooks/scribe-stop.sh +67 -0
  44. package/plugins/scribe/scripts/lib/scribe-config.sh +72 -0
  45. package/plugins/scribe/scripts/lib/scribe-distill.sh +239 -0
  46. package/plugins/scribe/scripts/lib/scribe-events.sh +80 -0
  47. package/plugins/scribe/scripts/lib/scribe-extract.sh +147 -0
  48. package/plugins/scribe/scripts/lib/scribe-project-key.sh +89 -0
  49. package/plugins/scribe/scripts/lib/scribe-ulid.sh +50 -0
  50. package/release-please-config.json +48 -0
  51. package/test/bats/counsel-project-key.bats +82 -0
  52. package/test/bats/counsel-reader.bats +132 -0
  53. package/test/bats/scribe-extract.bats +102 -0
  54. package/test/bats/scribe-project-key.bats +75 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "counsel",
3
+ "version": "0.2.0",
4
+ "description": "Weekly synthesis and recommendations from the full observability stack. Reads all plugin event logs, produces a structured improvement brief, and injects it at session start when the last brief is stale.",
5
+ "author": {
6
+ "name": "Onlooker Community",
7
+ "url": "https://onlooker.dev"
8
+ },
9
+ "homepage": "https://onlooker.dev",
10
+ "repository": "https://github.com/onlooker-community/ecosystem",
11
+ "license": "MIT",
12
+ "skills": [],
13
+ "agents": []
14
+ }
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/onlooker-community/ecosystem/compare/counsel-v0.1.0...counsel-v0.2.0) (2026-06-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * **counsel:** weekly observability synthesis and coaching brief :robot: ([#51](https://github.com/onlooker-community/ecosystem/issues/51)) ([6364586](https://github.com/onlooker-community/ecosystem/commit/63645863cf3a1d7bbf0353aacb9b71e4f977dd56))
@@ -0,0 +1,20 @@
1
+ {
2
+ "counsel": {
3
+ "enabled": true,
4
+ "synthesis_interval_days": 7,
5
+ "lookback_days": 30,
6
+ "evaluator": {
7
+ "model": "claude-haiku-4-5-20251001",
8
+ "timeout": 90,
9
+ "max_tokens": 4096,
10
+ "temperature": 0.4
11
+ },
12
+ "capture": {
13
+ "min_events": 10,
14
+ "events_chars_max": 60000
15
+ },
16
+ "output": {
17
+ "brief_max_chars": 3000
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/counsel-session-start.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # Counsel SessionStart hook — weekly improvement brief injection.
3
+ #
4
+ # Fires at session start. If the last brief for this project is older than
5
+ # synthesis_interval_days (default: 7), runs a Haiku synthesis pass over the
6
+ # full event log and injects the resulting brief as additionalContext.
7
+ #
8
+ # Skip conditions (all silent):
9
+ # - counsel.enabled is false
10
+ # - no project key (non-git directory)
11
+ # - brief is still fresh
12
+ # - fewer than min_events events in the lookback window
13
+ #
14
+ # Hook contract:
15
+ # - Always exits 0. Never blocks session start.
16
+ # - Emits hookSpecificOutput JSON on stdout (even when context is empty).
17
+ # - Errors are written to stderr only.
18
+
19
+ set -uo pipefail
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
23
+
24
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
25
+
26
+ # shellcheck source=../lib/counsel-config.sh
27
+ source "${PLUGIN_ROOT}/scripts/lib/counsel-config.sh"
28
+ # shellcheck source=../lib/counsel-events.sh
29
+ source "${PLUGIN_ROOT}/scripts/lib/counsel-events.sh"
30
+ # shellcheck source=../lib/counsel-project-key.sh
31
+ source "${PLUGIN_ROOT}/scripts/lib/counsel-project-key.sh"
32
+ # shellcheck source=../lib/counsel-ulid.sh
33
+ source "${PLUGIN_ROOT}/scripts/lib/counsel-ulid.sh"
34
+ # shellcheck source=../lib/counsel-reader.sh
35
+ source "${PLUGIN_ROOT}/scripts/lib/counsel-reader.sh"
36
+ # shellcheck source=../lib/counsel-synthesize.sh
37
+ source "${PLUGIN_ROOT}/scripts/lib/counsel-synthesize.sh"
38
+ # shellcheck source=../lib/counsel-brief.sh
39
+ source "${PLUGIN_ROOT}/scripts/lib/counsel-brief.sh"
40
+
41
+ _emit() {
42
+ local context="${1:-}"
43
+ jq -cn --arg ctx "$context" '
44
+ {
45
+ hookSpecificOutput: {
46
+ hookEventName: "SessionStart",
47
+ additionalContext: $ctx
48
+ }
49
+ }
50
+ '
51
+ }
52
+
53
+ INPUT=$(cat)
54
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
55
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
56
+
57
+ export _HOOK_SESSION_ID="$SESSION_ID"
58
+
59
+ REPO_ROOT=$(counsel_project_repo_root "$CWD")
60
+ counsel_config_load "$REPO_ROOT"
61
+
62
+ if ! counsel_config_enabled; then
63
+ _emit ""
64
+ exit 0
65
+ fi
66
+
67
+ PROJECT_KEY=$(counsel_project_key "$CWD")
68
+ if [[ -z "$PROJECT_KEY" ]]; then
69
+ _emit ""
70
+ exit 0
71
+ fi
72
+
73
+ _generate_rc=0
74
+ OUTPUT_PATH=$(counsel_generate_brief "$SESSION_ID" "$CWD") || _generate_rc=$?
75
+
76
+ if [[ $_generate_rc -ne 0 ]]; then
77
+ # rc=2 means stale check passed (brief is fresh) or too few events — silent skip.
78
+ [[ $_generate_rc -ne 2 ]] && printf 'counsel-session-start: brief generation failed for session %s\n' "$SESSION_ID" >&2
79
+ _emit ""
80
+ exit 0
81
+ fi
82
+
83
+ if [[ -z "$OUTPUT_PATH" || ! -f "$OUTPUT_PATH" ]]; then
84
+ _emit ""
85
+ exit 0
86
+ fi
87
+
88
+ # Load the brief content and apply the configured char budget.
89
+ BRIEF_MAX_CHARS=$(counsel_config_get '.counsel.output.brief_max_chars')
90
+ [[ -z "$BRIEF_MAX_CHARS" || "$BRIEF_MAX_CHARS" == "null" ]] && BRIEF_MAX_CHARS="3000"
91
+
92
+ BRIEF_CONTENT=$(head -c "$BRIEF_MAX_CHARS" "$OUTPUT_PATH" 2>/dev/null) || BRIEF_CONTENT=""
93
+
94
+ if [[ -z "$BRIEF_CONTENT" ]]; then
95
+ _emit ""
96
+ exit 0
97
+ fi
98
+
99
+ CONTEXT="Counsel — weekly improvement brief (auto-generated from your onlooker event log):
100
+
101
+ ${BRIEF_CONTENT}
102
+
103
+ (Counsel injected this brief for project key ${PROJECT_KEY}. Set counsel.enabled=false to disable.)"
104
+
105
+ _emit "$CONTEXT"
106
+ exit 0
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env bash
2
+ # Brief orchestrator for Counsel.
3
+ #
4
+ # Checks whether a fresh brief is needed, runs the full synthesis pipeline,
5
+ # writes a Markdown brief to ~/.onlooker/counsel/<project-key>/briefs/,
6
+ # and emits counsel.brief.generated.
7
+ #
8
+ # Exposes:
9
+ # counsel_brief_is_stale <project_key> <interval_days>
10
+ # Returns 0 if a new brief should be generated, 1 if the existing one is fresh.
11
+ #
12
+ # counsel_generate_brief <session_id> <cwd>
13
+ # Runs the full pipeline. Echoes the output path on success.
14
+ # Returns 2 if skipped (not stale or too few events).
15
+ # Returns 1 on hard failure.
16
+
17
+ # shellcheck source=./counsel-reader.sh
18
+ # shellcheck source=./counsel-synthesize.sh
19
+ # shellcheck source=./counsel-events.sh
20
+ # shellcheck source=./counsel-project-key.sh
21
+ # (caller must source these before counsel-brief.sh)
22
+
23
+ _counsel_latest_brief_path() {
24
+ local briefs_dir="${1:-}"
25
+ [[ -d "$briefs_dir" ]] || return 1
26
+ # Briefs are named YYYY-WW.md; newest sorts last lexicographically.
27
+ local latest
28
+ latest=$(ls -1 "$briefs_dir"/*.md 2>/dev/null | sort | tail -1)
29
+ [[ -n "$latest" ]] || return 1
30
+ printf '%s' "$latest"
31
+ }
32
+
33
+ _counsel_brief_mtime_epoch() {
34
+ local path="${1:-}"
35
+ [[ -f "$path" ]] || { printf '0'; return 0; }
36
+ if [[ "$(uname)" == "Darwin" ]]; then
37
+ stat -f '%m' "$path" 2>/dev/null || printf '0'
38
+ else
39
+ stat -c '%Y' "$path" 2>/dev/null || printf '0'
40
+ fi
41
+ }
42
+
43
+ counsel_brief_is_stale() {
44
+ local project_key="${1:-}"
45
+ local interval_days="${2:-7}"
46
+
47
+ local briefs_dir
48
+ briefs_dir=$(counsel_project_dir "$project_key")
49
+
50
+ local latest_path
51
+ latest_path=$(_counsel_latest_brief_path "$briefs_dir") || { return 0; }
52
+
53
+ local mtime now age_days
54
+ mtime=$(_counsel_brief_mtime_epoch "$latest_path")
55
+ now=$(date +%s 2>/dev/null) || now=0
56
+ age_days=$(( (now - mtime) / 86400 ))
57
+
58
+ [[ "$age_days" -ge "$interval_days" ]]
59
+ }
60
+
61
+ _counsel_format_brief() {
62
+ local brief_json="${1:-}"
63
+ local period_start="${2:-}"
64
+ local period_end="${3:-}"
65
+ local event_count="${4:-0}"
66
+
67
+ local summary patterns_json recs_json wins_json watch_json
68
+ summary=$(printf '%s' "$brief_json" | jq -r '.summary // ""' 2>/dev/null) || summary=""
69
+ patterns_json=$(printf '%s' "$brief_json" | jq -c '.patterns // []' 2>/dev/null) || patterns_json="[]"
70
+ recs_json=$(printf '%s' "$brief_json" | jq -c '.recommendations // []' 2>/dev/null) || recs_json="[]"
71
+ wins_json=$(printf '%s' "$brief_json" | jq -c '.wins // []' 2>/dev/null) || wins_json="[]"
72
+ watch_json=$(printf '%s' "$brief_json" | jq -c '.watch // []' 2>/dev/null) || watch_json="[]"
73
+
74
+ local timestamp
75
+ timestamp=$(date '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || timestamp="unknown"
76
+
77
+ {
78
+ printf '# Counsel Brief: %s — %s\n\n' "$period_start" "$period_end"
79
+ [[ -n "$summary" ]] && printf '> %s\n\n' "$summary"
80
+
81
+ printf '## Recommendations\n\n'
82
+ local rec_count
83
+ rec_count=$(printf '%s' "$recs_json" | jq 'length' 2>/dev/null) || rec_count=0
84
+ if [[ "$rec_count" -gt 0 ]]; then
85
+ local i
86
+ for ((i = 0; i < rec_count; i++)); do
87
+ local title rationale priority
88
+ title=$(printf '%s' "$recs_json" | jq -r ".[$i].title // \"\"" 2>/dev/null) || title=""
89
+ rationale=$(printf '%s' "$recs_json" | jq -r ".[$i].rationale // \"\"" 2>/dev/null) || rationale=""
90
+ priority=$(printf '%s' "$recs_json" | jq -r ".[$i].priority // \"medium\"" 2>/dev/null) || priority="medium"
91
+ [[ -z "$title" ]] && continue
92
+ printf '### [%s] %s\n\n' "$priority" "$title"
93
+ [[ -n "$rationale" ]] && printf '%s\n\n' "$rationale"
94
+ done
95
+ else
96
+ printf '*No recommendations this period.*\n\n'
97
+ fi
98
+
99
+ printf '## Patterns Observed\n\n'
100
+ local pat_count
101
+ pat_count=$(printf '%s' "$patterns_json" | jq 'length' 2>/dev/null) || pat_count=0
102
+ if [[ "$pat_count" -gt 0 ]]; then
103
+ printf '%s' "$patterns_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
104
+ [[ -n "$item" ]] && printf '- %s\n' "$item"
105
+ done
106
+ printf '\n'
107
+ else
108
+ printf '*None identified.*\n\n'
109
+ fi
110
+
111
+ printf '## Wins\n\n'
112
+ local win_count
113
+ win_count=$(printf '%s' "$wins_json" | jq 'length' 2>/dev/null) || win_count=0
114
+ if [[ "$win_count" -gt 0 ]]; then
115
+ printf '%s' "$wins_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
116
+ [[ -n "$item" ]] && printf '- %s\n' "$item"
117
+ done
118
+ printf '\n'
119
+ else
120
+ printf '*None noted.*\n\n'
121
+ fi
122
+
123
+ printf '## Watch\n\n'
124
+ local watch_count
125
+ watch_count=$(printf '%s' "$watch_json" | jq 'length' 2>/dev/null) || watch_count=0
126
+ if [[ "$watch_count" -gt 0 ]]; then
127
+ printf '%s' "$watch_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
128
+ [[ -n "$item" ]] && printf '- %s\n' "$item"
129
+ done
130
+ printf '\n'
131
+ else
132
+ printf '*Nothing flagged.*\n\n'
133
+ fi
134
+
135
+ printf '---\n'
136
+ printf '*Generated by counsel · %s · %d events analyzed*\n' "$timestamp" "$event_count"
137
+ }
138
+ }
139
+
140
+ counsel_generate_brief() {
141
+ local session_id="${1:-}"
142
+ local cwd="${2:-}"
143
+
144
+ [[ -z "$session_id" ]] && return 1
145
+
146
+ local onlooker_dir="${ONLOOKER_DIR:-${HOME}/.onlooker}"
147
+
148
+ # Resolve config values.
149
+ local interval_days lookback_days model timeout_s max_tokens temperature chars_max min_events
150
+ interval_days=$(counsel_config_get '.counsel.synthesis_interval_days')
151
+ [[ -z "$interval_days" || "$interval_days" == "null" ]] && interval_days="7"
152
+ lookback_days=$(counsel_config_get '.counsel.lookback_days')
153
+ [[ -z "$lookback_days" || "$lookback_days" == "null" ]] && lookback_days="30"
154
+ model=$(counsel_config_get '.counsel.evaluator.model')
155
+ [[ -z "$model" || "$model" == "null" ]] && model="claude-haiku-4-5-20251001"
156
+ timeout_s=$(counsel_config_get '.counsel.evaluator.timeout')
157
+ [[ -z "$timeout_s" || "$timeout_s" == "null" ]] && timeout_s="90"
158
+ max_tokens=$(counsel_config_get '.counsel.evaluator.max_tokens')
159
+ [[ -z "$max_tokens" || "$max_tokens" == "null" ]] && max_tokens="4096"
160
+ temperature=$(counsel_config_get '.counsel.evaluator.temperature')
161
+ [[ -z "$temperature" || "$temperature" == "null" ]] && temperature="0.4"
162
+ chars_max=$(counsel_config_get '.counsel.capture.events_chars_max')
163
+ [[ -z "$chars_max" || "$chars_max" == "null" ]] && chars_max="60000"
164
+ min_events=$(counsel_config_get '.counsel.capture.min_events')
165
+ [[ -z "$min_events" || "$min_events" == "null" ]] && min_events="10"
166
+
167
+ local project_key
168
+ project_key=$(counsel_project_key "$cwd")
169
+
170
+ # Stale check.
171
+ if ! counsel_brief_is_stale "$project_key" "$interval_days"; then
172
+ return 2
173
+ fi
174
+
175
+ # Read events.
176
+ local events_text
177
+ events_text=$(counsel_read_events "$lookback_days" "$chars_max")
178
+
179
+ local event_count
180
+ event_count=$(counsel_count_events "$events_text")
181
+
182
+ if [[ "$event_count" -lt "$min_events" ]]; then
183
+ return 2
184
+ fi
185
+
186
+ # Run synthesis.
187
+ local brief_json
188
+ brief_json=$(counsel_synthesize "$events_text" "$model" "$timeout_s" "$max_tokens" "$temperature") || {
189
+ printf 'counsel_generate_brief: synthesis failed for session %s\n' "$session_id" >&2
190
+ return 1
191
+ }
192
+
193
+ # Compute period bounds.
194
+ local period_start period_end
195
+ period_end=$(date '+%Y-%m-%d' 2>/dev/null) || period_end="unknown"
196
+ if [[ "$(uname)" == "Darwin" ]]; then
197
+ period_start=$(date -v "-${lookback_days}d" '+%Y-%m-%d' 2>/dev/null) || period_start="unknown"
198
+ else
199
+ period_start=$(date -d "-${lookback_days} days" '+%Y-%m-%d' 2>/dev/null) || period_start="unknown"
200
+ fi
201
+
202
+ # Determine sources consulted.
203
+ local sources_json
204
+ sources_json=$(counsel_sources_from_events "$events_text")
205
+
206
+ # Write brief.
207
+ local briefs_dir output_path
208
+ if [[ -n "$project_key" ]]; then
209
+ briefs_dir=$(counsel_project_dir "$project_key")
210
+ else
211
+ briefs_dir="${onlooker_dir}/counsel/unknown/briefs"
212
+ fi
213
+
214
+ mkdir -p "$briefs_dir" 2>/dev/null || {
215
+ printf 'counsel_generate_brief: cannot create briefs dir %s\n' "$briefs_dir" >&2
216
+ return 1
217
+ }
218
+
219
+ local week_label
220
+ week_label=$(date '+%G-%V' 2>/dev/null) || week_label=$(date '+%Y-%U' 2>/dev/null) || week_label="unknown"
221
+ local filename="${week_label}.md"
222
+ output_path="${briefs_dir}/${filename}"
223
+
224
+ local doc
225
+ doc=$(_counsel_format_brief "$brief_json" "$period_start" "$period_end" "$event_count")
226
+
227
+ printf '%s\n' "$doc" > "$output_path" 2>/dev/null || {
228
+ printf 'counsel_generate_brief: failed to write %s\n' "$output_path" >&2
229
+ return 1
230
+ }
231
+
232
+ # Emit counsel.brief.generated.
233
+ local rec_count
234
+ rec_count=$(printf '%s' "$brief_json" | jq '.recommendations | length' 2>/dev/null) || rec_count=0
235
+
236
+ local payload
237
+ payload=$(jq -n \
238
+ --arg ps "$period_start" \
239
+ --arg pe "$period_end" \
240
+ --argjson rc "$rec_count" \
241
+ --argjson src "$sources_json" \
242
+ '{period_start: $ps, period_end: $pe, recommendation_count: $rc, sources_consulted: $src}') || payload=""
243
+
244
+ [[ -n "$payload" ]] && counsel_emit_event "counsel.brief.generated" "$payload" || true
245
+
246
+ printf '%s' "$output_path"
247
+ }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Config resolution for Counsel.
3
+ #
4
+ # Three-layer merge, latest wins:
5
+ # 1. plugins/counsel/config.json (plugin defaults)
6
+ # 2. ~/.claude/settings.json
7
+ # 3. <repo>/.claude/settings.json
8
+ #
9
+ # Exposes:
10
+ # counsel_config_load <repo_root> # populates _COUNSEL_CONFIG (JSON)
11
+ # counsel_config_get <jq-path> # echoes string value (empty if unset)
12
+ # counsel_config_get_json <jq-path> # echoes JSON value (null if unset)
13
+ # counsel_config_enabled # 0 if counsel.enabled is true
14
+
15
+ _COUNSEL_CONFIG="{}"
16
+
17
+ counsel_config_load() {
18
+ local repo_root="${1:-}"
19
+ local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
20
+ local home_dir="${HOME:-}"
21
+
22
+ local merged="{}"
23
+ local file
24
+
25
+ file="${plugin_root}/config.json"
26
+ if [[ -f "$file" ]]; then
27
+ local defaults
28
+ defaults=$(jq '.' "$file" 2>/dev/null) || defaults="{}"
29
+ merged=$(jq -n --argjson a "$merged" --argjson b "$defaults" '$a * $b' 2>/dev/null) \
30
+ || merged="$defaults"
31
+ fi
32
+
33
+ local repo_settings=""
34
+ [[ -n "$repo_root" ]] && repo_settings="${repo_root}/.claude/settings.json"
35
+
36
+ for file in "${home_dir}/.claude/settings.json" "$repo_settings"; do
37
+ [[ -n "$file" && -f "$file" ]] || continue
38
+ local overlay
39
+ overlay=$(jq '{ counsel: (.counsel // {}) }' "$file" 2>/dev/null) || continue
40
+ [[ -z "$overlay" ]] && continue
41
+ local attempt
42
+ if attempt=$(jq -n --argjson a "$merged" --argjson b "$overlay" '
43
+ def deepmerge($a; $b):
44
+ if ($a|type) == "object" and ($b|type) == "object" then
45
+ reduce (($a|keys) + ($b|keys) | unique)[] as $k
46
+ ({}; .[$k] = deepmerge($a[$k]; $b[$k]))
47
+ elif $b == null then $a
48
+ else $b end;
49
+ deepmerge($a; $b)
50
+ ' 2>/dev/null) && [[ -n "$attempt" ]]; then
51
+ merged="$attempt"
52
+ fi
53
+ done
54
+
55
+ _COUNSEL_CONFIG="$merged"
56
+ }
57
+
58
+ counsel_config_get() {
59
+ local path="$1"
60
+ printf '%s' "$_COUNSEL_CONFIG" | jq -r "${path} // empty" 2>/dev/null
61
+ }
62
+
63
+ counsel_config_get_json() {
64
+ local path="$1"
65
+ printf '%s' "$_COUNSEL_CONFIG" | jq -c "${path}" 2>/dev/null
66
+ }
67
+
68
+ counsel_config_enabled() {
69
+ local v
70
+ v=$(counsel_config_get '.counsel.enabled')
71
+ [[ "$v" == "true" ]]
72
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # Canonical counsel.* event emission.
3
+ #
4
+ # Thin wrapper around the ecosystem plugin's onlooker-event.mjs `emit` mode.
5
+ # Every emission is validated against @onlooker-community/schema before being
6
+ # appended to $ONLOOKER_EVENTS_LOG (defaults to $ONLOOKER_DIR/logs/onlooker-events.jsonl).
7
+ #
8
+ # Usage:
9
+ # counsel_emit_event "counsel.brief.generated" '{"period_start":"...","period_end":"...","recommendation_count":3}'
10
+
11
+ _COUNSEL_PLUGIN_NAME="counsel"
12
+
13
+ _counsel_event_js_path() {
14
+ if [[ -n "${_ONLOOKER_EVENT_JS:-}" && -f "$_ONLOOKER_EVENT_JS" ]]; then
15
+ printf '%s' "$_ONLOOKER_EVENT_JS"
16
+ return 0
17
+ fi
18
+ local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
19
+ local candidates=(
20
+ "${plugin_root}/scripts/lib/onlooker-event.mjs"
21
+ "${plugin_root}/../../scripts/lib/onlooker-event.mjs"
22
+ )
23
+ local c
24
+ for c in "${candidates[@]}"; do
25
+ [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
26
+ done
27
+ return 1
28
+ }
29
+
30
+ _counsel_session_id() {
31
+ if [[ -n "${_HOOK_SESSION_ID:-}" ]]; then
32
+ printf '%s' "$_HOOK_SESSION_ID"
33
+ return 0
34
+ fi
35
+ if [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then
36
+ printf '%s' "$CLAUDE_SESSION_ID"
37
+ return 0
38
+ fi
39
+ printf 'unknown'
40
+ }
41
+
42
+ counsel_emit_event() {
43
+ local event_type="${1:-}"
44
+ local payload="${2:-}"
45
+
46
+ [[ -z "$event_type" || -z "$payload" ]] && return 1
47
+
48
+ local event_js
49
+ event_js=$(_counsel_event_js_path) || return 1
50
+
51
+ local session_id
52
+ session_id=$(_counsel_session_id)
53
+
54
+ local params
55
+ params=$(jq -n \
56
+ --arg plugin "$_COUNSEL_PLUGIN_NAME" \
57
+ --arg sid "$session_id" \
58
+ --arg type "$event_type" \
59
+ --argjson payload "$payload" \
60
+ '{plugin: $plugin, session_id: $sid, event_type: $type, payload: $payload}' \
61
+ 2>/dev/null) || return 1
62
+
63
+ local event
64
+ local stderr_file
65
+ stderr_file=$(mktemp -t counsel-event-err.XXXXXX 2>/dev/null) || stderr_file="/tmp/counsel-event-err.$$"
66
+ event=$(printf '%s' "$params" \
67
+ | ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}" \
68
+ ONLOOKER_PLUGIN_NAME="$_COUNSEL_PLUGIN_NAME" \
69
+ node "$event_js" emit 2>"$stderr_file") || {
70
+ printf 'counsel_emit_event: schema validation failed for %s\n' "$event_type" >&2
71
+ [[ -s "$stderr_file" ]] && cat "$stderr_file" >&2
72
+ rm -f "$stderr_file"
73
+ return 1
74
+ }
75
+ rm -f "$stderr_file"
76
+
77
+ local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
78
+ mkdir -p "$(dirname "$log_path")" 2>/dev/null || return 1
79
+ printf '%s\n' "$event" >> "$log_path"
80
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # Project key derivation for Counsel.
3
+ #
4
+ # Mirrors the tribunal/scribe project-key scheme so plugins partition storage
5
+ # identically. A project key is a stable 12-char hex identifier derived from
6
+ # the git remote URL (preferred) or repo root path (fallback).
7
+
8
+ _counsel_sha256_first12() {
9
+ local input="$1"
10
+ if command -v shasum >/dev/null 2>&1; then
11
+ printf '%s' "$input" | shasum -a 256 2>/dev/null | cut -c1-12
12
+ elif command -v sha256sum >/dev/null 2>&1; then
13
+ printf '%s' "$input" | sha256sum 2>/dev/null | cut -c1-12
14
+ else
15
+ return 1
16
+ fi
17
+ }
18
+
19
+ counsel_project_remote_url() {
20
+ local cwd="${1:-}"
21
+ [[ -z "$cwd" || ! -d "$cwd" ]] && return 0
22
+ git -C "$cwd" remote get-url origin 2>/dev/null || true
23
+ }
24
+
25
+ # Worktree-aware: uses common-dir so worktrees share a key with the main repo.
26
+ counsel_project_repo_root() {
27
+ local cwd="${1:-}"
28
+ [[ -z "$cwd" || ! -d "$cwd" ]] && return 0
29
+
30
+ if ! git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
31
+ return 0
32
+ fi
33
+
34
+ local common_dir toplevel
35
+ common_dir=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || return 0
36
+
37
+ if [[ -n "$common_dir" && "$common_dir" != /* ]]; then
38
+ common_dir="$(cd "$cwd" && cd "$common_dir" 2>/dev/null && pwd -P)" || common_dir=""
39
+ fi
40
+
41
+ if [[ -n "$common_dir" && -d "$common_dir" ]]; then
42
+ toplevel="$(cd "$common_dir/.." 2>/dev/null && pwd -P)" || toplevel=""
43
+ fi
44
+
45
+ if [[ -z "$toplevel" ]]; then
46
+ toplevel=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true)
47
+ [[ -n "$toplevel" ]] && toplevel="$(cd "$toplevel" 2>/dev/null && pwd -P)"
48
+ fi
49
+
50
+ printf '%s' "$toplevel"
51
+ }
52
+
53
+ counsel_project_key() {
54
+ local cwd="${1:-}"
55
+ [[ -z "$cwd" ]] && cwd="$(pwd)"
56
+
57
+ local remote
58
+ remote=$(counsel_project_remote_url "$cwd")
59
+ if [[ -n "$remote" ]]; then
60
+ _counsel_sha256_first12 "remote:$remote"
61
+ return 0
62
+ fi
63
+
64
+ local root
65
+ root=$(counsel_project_repo_root "$cwd")
66
+ if [[ -n "$root" ]]; then
67
+ _counsel_sha256_first12 "root:$root"
68
+ return 0
69
+ fi
70
+
71
+ return 0
72
+ }
73
+
74
+ counsel_project_dir() {
75
+ local project_key="${1:-}"
76
+ [[ -z "$project_key" ]] && return 1
77
+ local onlooker_dir="${ONLOOKER_DIR:-${HOME}/.onlooker}"
78
+ printf '%s' "${onlooker_dir}/counsel/${project_key}/briefs"
79
+ }