@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.
- package/.claude-plugin/marketplace.json +39 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +5 -2
- package/CHANGELOG.md +15 -0
- package/CLAUDE.md +88 -0
- package/package.json +2 -2
- package/plugins/compass/.claude-plugin/plugin.json +14 -0
- package/plugins/compass/CHANGELOG.md +8 -0
- package/plugins/compass/config.json +71 -0
- package/plugins/compass/docs/adr/001-evaluate-prompts-in-context.md +82 -0
- package/plugins/compass/docs/design.md +421 -0
- package/plugins/compass/hooks/hooks.json +82 -0
- package/plugins/compass/scripts/hooks/compass-bash-gate.sh +95 -0
- package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +86 -0
- package/plugins/compass/scripts/hooks/compass-record-write.sh +97 -0
- package/plugins/compass/scripts/hooks/compass-session-start.sh +77 -0
- package/plugins/compass/scripts/lib/compass-config.sh +72 -0
- package/plugins/compass/scripts/lib/compass-evaluator.sh +374 -0
- package/plugins/compass/scripts/lib/compass-events.sh +81 -0
- package/plugins/compass/scripts/lib/compass-gate.sh +465 -0
- package/plugins/compass/scripts/lib/compass-sanitizer.sh +82 -0
- package/plugins/compass/scripts/lib/compass-transcript.sh +135 -0
- package/plugins/counsel/.claude-plugin/plugin.json +14 -0
- package/plugins/counsel/CHANGELOG.md +8 -0
- package/plugins/counsel/config.json +20 -0
- package/plugins/counsel/hooks/hooks.json +15 -0
- package/plugins/counsel/scripts/hooks/counsel-session-start.sh +106 -0
- package/plugins/counsel/scripts/lib/counsel-brief.sh +247 -0
- package/plugins/counsel/scripts/lib/counsel-config.sh +72 -0
- package/plugins/counsel/scripts/lib/counsel-events.sh +80 -0
- package/plugins/counsel/scripts/lib/counsel-project-key.sh +79 -0
- package/plugins/counsel/scripts/lib/counsel-reader.sh +114 -0
- package/plugins/counsel/scripts/lib/counsel-synthesize.sh +103 -0
- package/plugins/counsel/scripts/lib/counsel-ulid.sh +45 -0
- package/plugins/governor/.claude-plugin/plugin.json +1 -1
- package/plugins/governor/CHANGELOG.md +7 -0
- package/plugins/scribe/.claude-plugin/plugin.json +12 -0
- package/plugins/scribe/CHANGELOG.md +8 -0
- package/plugins/scribe/config.json +20 -0
- package/plugins/scribe/hooks/hooks.json +37 -0
- package/plugins/scribe/scripts/hooks/scribe-capture.sh +76 -0
- package/plugins/scribe/scripts/hooks/scribe-session-start.sh +58 -0
- package/plugins/scribe/scripts/hooks/scribe-stop.sh +67 -0
- package/plugins/scribe/scripts/lib/scribe-config.sh +72 -0
- package/plugins/scribe/scripts/lib/scribe-distill.sh +239 -0
- package/plugins/scribe/scripts/lib/scribe-events.sh +80 -0
- package/plugins/scribe/scripts/lib/scribe-extract.sh +147 -0
- package/plugins/scribe/scripts/lib/scribe-project-key.sh +89 -0
- package/plugins/scribe/scripts/lib/scribe-ulid.sh +50 -0
- package/release-please-config.json +48 -0
- package/test/bats/counsel-project-key.bats +82 -0
- package/test/bats/counsel-reader.bats +132 -0
- package/test/bats/scribe-extract.bats +102 -0
- 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,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
|
+
}
|