@onlooker-community/ecosystem 0.25.1 → 0.26.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/plugin.json +1 -1
- package/.release-please-manifest.json +2 -2
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/plugins/counsel/.claude-plugin/plugin.json +2 -2
- package/plugins/counsel/CHANGELOG.md +7 -0
- package/plugins/counsel/README.md +12 -0
- package/plugins/counsel/scripts/lib/counsel-brief.sh +41 -16
- package/plugins/counsel/skills/counsel/SKILL.md +146 -0
- package/test/bats/counsel-brief.bats +167 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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.
|
|
2
|
+
".": "0.26.0",
|
|
3
3
|
"plugins/archivist": "0.1.0",
|
|
4
4
|
"plugins/tribunal": "1.0.1",
|
|
5
5
|
"plugins/echo": "0.2.0",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"plugins/governor": "0.2.1",
|
|
8
8
|
"plugins/compass": "0.2.0",
|
|
9
9
|
"plugins/scribe": "0.2.1",
|
|
10
|
-
"plugins/counsel": "0.
|
|
10
|
+
"plugins/counsel": "0.3.0",
|
|
11
11
|
"plugins/warden": "0.2.0",
|
|
12
12
|
"plugins/librarian": "0.2.0",
|
|
13
13
|
"plugins/curator": "0.1.0",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.26.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.25.1...ecosystem-v0.26.0) (2026-06-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **counsel:** add /counsel on-demand weekly-review command :rocket: ([#76](https://github.com/onlooker-community/ecosystem/issues/76)) ([8ce951c](https://github.com/onlooker-community/ecosystem/commit/8ce951cd5cb7b173f194f86c2960a31fb0d6889d))
|
|
9
|
+
|
|
3
10
|
## [0.25.1](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.25.0...ecosystem-v0.25.1) (2026-06-10)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counsel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
"homepage": "https://onlooker.dev",
|
|
10
10
|
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"skills": [],
|
|
12
|
+
"skills": ["./skills/counsel"],
|
|
13
13
|
"agents": []
|
|
14
14
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/onlooker-community/ecosystem/compare/counsel-v0.2.0...counsel-v0.3.0) (2026-06-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **counsel:** add /counsel on-demand weekly-review command :rocket: ([#76](https://github.com/onlooker-community/ecosystem/issues/76)) ([8ce951c](https://github.com/onlooker-community/ecosystem/commit/8ce951cd5cb7b173f194f86c2960a31fb0d6889d))
|
|
9
|
+
|
|
3
10
|
## [0.2.0](https://github.com/onlooker-community/ecosystem/compare/counsel-v0.1.0...counsel-v0.2.0) (2026-06-02)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -18,6 +18,18 @@ Counsel partitions the event stream by source plugin, recognizing `tribunal`, `e
|
|
|
18
18
|
|
|
19
19
|
The hook always exits 0 — it never blocks a session from starting. It skips silently when Counsel is disabled, the directory has no project key (non-git), the latest brief is still fresh, or fewer than `capture.min_events` events fall inside the lookback window.
|
|
20
20
|
|
|
21
|
+
## On-demand brief — `/counsel`
|
|
22
|
+
|
|
23
|
+
The SessionStart path only regenerates when the latest brief is stale. To run the weekly review immediately — regardless of freshness — invoke the `/counsel` skill. It forces a synthesis pass, writes the brief, emits `counsel.brief.generated`, and renders the result in the conversation instead of injecting it invisibly.
|
|
24
|
+
|
|
25
|
+
| Invocation | What it does |
|
|
26
|
+
|------------|--------------|
|
|
27
|
+
| `/counsel` | Forces a fresh synthesis now (bypassing the staleness gate), writes `<YYYY-WW>.md`, and prints the brief. Re-running in the same ISO week overwrites that week's brief in place. |
|
|
28
|
+
| `/counsel --show` | Renders the most recent brief already on disk. No LLM call, no events emitted. |
|
|
29
|
+
| `/counsel --status` | Reports the latest brief's age, last-generated time, and whether it is stale. No LLM call. |
|
|
30
|
+
|
|
31
|
+
The on-demand path bypasses only the staleness gate — output, events, storage layout, project keying, and the `capture.min_events` floor are identical to the SessionStart path. If too few events fall inside the lookback window, `/counsel` reports that rather than emitting a thin brief.
|
|
32
|
+
|
|
21
33
|
## Activation
|
|
22
34
|
|
|
23
35
|
Counsel is **on by default**. Disable it per-project in `.claude/settings.json` (or globally in `~/.claude/settings.json`):
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
# counsel_brief_is_stale <project_key> <interval_days>
|
|
10
10
|
# Returns 0 if a new brief should be generated, 1 if the existing one is fresh.
|
|
11
11
|
#
|
|
12
|
-
# counsel_generate_brief <session_id> <cwd>
|
|
12
|
+
# counsel_generate_brief <session_id> <cwd> [force]
|
|
13
13
|
# Runs the full pipeline. Echoes the output path on success.
|
|
14
|
+
# When [force] is "1" or "force", the staleness gate is bypassed (used by
|
|
15
|
+
# the on-demand /counsel skill); the min_events gate always applies.
|
|
14
16
|
# Returns 2 if skipped (not stale or too few events).
|
|
15
17
|
# Returns 1 on hard failure.
|
|
16
18
|
|
|
@@ -101,7 +103,7 @@ _counsel_format_brief() {
|
|
|
101
103
|
pat_count=$(printf '%s' "$patterns_json" | jq 'length' 2>/dev/null) || pat_count=0
|
|
102
104
|
if [[ "$pat_count" -gt 0 ]]; then
|
|
103
105
|
printf '%s' "$patterns_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
|
|
104
|
-
[[ -n "$item" ]] && printf '- %s\n' "$item"
|
|
106
|
+
[[ -n "$item" ]] && printf -- '- %s\n' "$item"
|
|
105
107
|
done
|
|
106
108
|
printf '\n'
|
|
107
109
|
else
|
|
@@ -113,7 +115,7 @@ _counsel_format_brief() {
|
|
|
113
115
|
win_count=$(printf '%s' "$wins_json" | jq 'length' 2>/dev/null) || win_count=0
|
|
114
116
|
if [[ "$win_count" -gt 0 ]]; then
|
|
115
117
|
printf '%s' "$wins_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
|
|
116
|
-
[[ -n "$item" ]] && printf '- %s\n' "$item"
|
|
118
|
+
[[ -n "$item" ]] && printf -- '- %s\n' "$item"
|
|
117
119
|
done
|
|
118
120
|
printf '\n'
|
|
119
121
|
else
|
|
@@ -125,14 +127,14 @@ _counsel_format_brief() {
|
|
|
125
127
|
watch_count=$(printf '%s' "$watch_json" | jq 'length' 2>/dev/null) || watch_count=0
|
|
126
128
|
if [[ "$watch_count" -gt 0 ]]; then
|
|
127
129
|
printf '%s' "$watch_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
|
|
128
|
-
[[ -n "$item" ]] && printf '- %s\n' "$item"
|
|
130
|
+
[[ -n "$item" ]] && printf -- '- %s\n' "$item"
|
|
129
131
|
done
|
|
130
132
|
printf '\n'
|
|
131
133
|
else
|
|
132
134
|
printf '*Nothing flagged.*\n\n'
|
|
133
135
|
fi
|
|
134
136
|
|
|
135
|
-
printf '---\n'
|
|
137
|
+
printf -- '---\n'
|
|
136
138
|
printf '*Generated by counsel · %s · %d events analyzed*\n' "$timestamp" "$event_count"
|
|
137
139
|
}
|
|
138
140
|
}
|
|
@@ -140,6 +142,7 @@ _counsel_format_brief() {
|
|
|
140
142
|
counsel_generate_brief() {
|
|
141
143
|
local session_id="${1:-}"
|
|
142
144
|
local cwd="${2:-}"
|
|
145
|
+
local force="${3:-0}"
|
|
143
146
|
|
|
144
147
|
[[ -z "$session_id" ]] && return 1
|
|
145
148
|
|
|
@@ -167,9 +170,11 @@ counsel_generate_brief() {
|
|
|
167
170
|
local project_key
|
|
168
171
|
project_key=$(counsel_project_key "$cwd")
|
|
169
172
|
|
|
170
|
-
# Stale check.
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
+
# Stale check. On-demand invocations pass force to bypass it.
|
|
174
|
+
if [[ "$force" != "1" && "$force" != "force" ]]; then
|
|
175
|
+
if ! counsel_brief_is_stale "$project_key" "$interval_days"; then
|
|
176
|
+
return 2
|
|
177
|
+
fi
|
|
173
178
|
fi
|
|
174
179
|
|
|
175
180
|
# Read events.
|
|
@@ -190,14 +195,28 @@ counsel_generate_brief() {
|
|
|
190
195
|
return 1
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
# Compute period bounds.
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
# Compute period bounds. The brief heading shows calendar dates; the emitted
|
|
199
|
+
# event requires RFC 3339 date-time strings (schema format: date-time), so
|
|
200
|
+
# compute the timestamps first and derive the date-only strings from them.
|
|
201
|
+
# Prefer UTC; fall back to local time (still RFC 3339, just a different zone)
|
|
202
|
+
# if -u is unavailable. If even that fails the date subsystem is broken and
|
|
203
|
+
# the bounds stay empty — the emit below skips rather than send garbage.
|
|
204
|
+
local period_start_ts period_end_ts
|
|
205
|
+
period_end_ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
206
|
+
|| period_end_ts=$(date '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
207
|
+
|| period_end_ts=""
|
|
196
208
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
197
|
-
|
|
209
|
+
period_start_ts=$(date -u -v "-${lookback_days}d" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
210
|
+
|| period_start_ts=$(date -v "-${lookback_days}d" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
211
|
+
|| period_start_ts=""
|
|
198
212
|
else
|
|
199
|
-
|
|
213
|
+
period_start_ts=$(date -u -d "-${lookback_days} days" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
214
|
+
|| period_start_ts=$(date -d "-${lookback_days} days" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
215
|
+
|| period_start_ts=""
|
|
200
216
|
fi
|
|
217
|
+
local period_start period_end
|
|
218
|
+
period_end="${period_end_ts%%T*}"; [[ -z "$period_end" ]] && period_end="unknown"
|
|
219
|
+
period_start="${period_start_ts%%T*}"; [[ -z "$period_start" ]] && period_start="unknown"
|
|
201
220
|
|
|
202
221
|
# Determine sources consulted.
|
|
203
222
|
local sources_json
|
|
@@ -235,13 +254,19 @@ counsel_generate_brief() {
|
|
|
235
254
|
|
|
236
255
|
local payload
|
|
237
256
|
payload=$(jq -n \
|
|
238
|
-
--arg ps "$
|
|
239
|
-
--arg pe "$
|
|
257
|
+
--arg ps "$period_start_ts" \
|
|
258
|
+
--arg pe "$period_end_ts" \
|
|
240
259
|
--argjson rc "$rec_count" \
|
|
241
260
|
--argjson src "$sources_json" \
|
|
242
261
|
'{period_start: $ps, period_end: $pe, recommendation_count: $rc, sources_consulted: $src}') || payload=""
|
|
243
262
|
|
|
244
|
-
|
|
263
|
+
# Only emit when the period bounds are valid RFC 3339 date-times — emitting
|
|
264
|
+
# empty bounds would fail schema validation and silently drop the event.
|
|
265
|
+
if [[ -n "$payload" && -n "$period_start_ts" && -n "$period_end_ts" ]]; then
|
|
266
|
+
counsel_emit_event "counsel.brief.generated" "$payload" || true
|
|
267
|
+
else
|
|
268
|
+
printf 'counsel_generate_brief: skipped counsel.brief.generated (no valid period bounds)\n' >&2
|
|
269
|
+
fi
|
|
245
270
|
|
|
246
271
|
printf '%s' "$output_path"
|
|
247
272
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: counsel
|
|
3
|
+
description: Run the weekly observability synthesis on demand and render the coaching brief in the conversation. Reads the onlooker event log, runs a single synthesis pass, writes the brief, and prints it — bypassing the SessionStart staleness gate. Use when the user explicitly invokes /counsel, or wants a fresh improvement brief right now instead of waiting for the next stale-brief regeneration. Supports --show (print the latest brief, no LLM call) and --status.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Counsel Skill
|
|
7
|
+
|
|
8
|
+
Counsel's SessionStart hook regenerates a weekly improvement brief only when the
|
|
9
|
+
last one has gone stale (`synthesis_interval_days`, default 7) and injects it
|
|
10
|
+
invisibly. This skill is the on-demand path: it forces a fresh synthesis right
|
|
11
|
+
now and renders the brief into the conversation.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Run this once at the start. It sources the plugin helpers, loads config, and
|
|
16
|
+
resolves project context.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
21
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
22
|
+
|
|
23
|
+
source "$PLUGIN_ROOT/scripts/lib/counsel-config.sh"
|
|
24
|
+
source "$PLUGIN_ROOT/scripts/lib/counsel-events.sh"
|
|
25
|
+
source "$PLUGIN_ROOT/scripts/lib/counsel-project-key.sh"
|
|
26
|
+
source "$PLUGIN_ROOT/scripts/lib/counsel-ulid.sh"
|
|
27
|
+
source "$PLUGIN_ROOT/scripts/lib/counsel-reader.sh"
|
|
28
|
+
source "$PLUGIN_ROOT/scripts/lib/counsel-synthesize.sh"
|
|
29
|
+
source "$PLUGIN_ROOT/scripts/lib/counsel-brief.sh"
|
|
30
|
+
|
|
31
|
+
REPO_ROOT=$(counsel_project_repo_root "$(pwd)")
|
|
32
|
+
counsel_config_load "$REPO_ROOT"
|
|
33
|
+
|
|
34
|
+
if ! counsel_config_enabled; then
|
|
35
|
+
echo "Counsel is disabled. Set counsel.enabled=true in .claude/settings.json to enable."
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
PROJECT_KEY=$(counsel_project_key "$(pwd)")
|
|
40
|
+
if [[ -z "$PROJECT_KEY" ]]; then
|
|
41
|
+
echo "No project key — Counsel needs a git repository (remote or root) to scope briefs. Skipping."
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
BRIEFS_DIR=$(counsel_project_dir "$PROJECT_KEY")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Invocation Modes
|
|
48
|
+
|
|
49
|
+
### `/counsel` — run the weekly review now (default)
|
|
50
|
+
|
|
51
|
+
Forces a synthesis pass regardless of brief freshness, writes the brief to
|
|
52
|
+
`${ONLOOKER_DIR:-~/.onlooker}/counsel/<project-key>/briefs/<YYYY-WW>.md`, emits
|
|
53
|
+
`counsel.brief.generated`, and renders the result. Re-running in the same ISO
|
|
54
|
+
week overwrites that week's brief in place. (`$ONLOOKER_DIR` overrides the
|
|
55
|
+
storage root; the test suite and non-default installs rely on it.)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
SESSION_ID="${CLAUDE_SESSION_ID:-$(counsel_ulid)}"
|
|
59
|
+
export _HOOK_SESSION_ID="$SESSION_ID"
|
|
60
|
+
|
|
61
|
+
LOOKBACK=$(counsel_config_get '.counsel.lookback_days'); LOOKBACK="${LOOKBACK:-30}"
|
|
62
|
+
echo "Running Counsel synthesis over the last ${LOOKBACK} days of events…"
|
|
63
|
+
|
|
64
|
+
_rc=0
|
|
65
|
+
OUTPUT_PATH=$(counsel_generate_brief "$SESSION_ID" "$(pwd)" force) || _rc=$?
|
|
66
|
+
|
|
67
|
+
if [[ "$_rc" -eq 2 ]]; then
|
|
68
|
+
min_events=$(counsel_config_get '.counsel.capture.min_events'); min_events="${min_events:-10}"
|
|
69
|
+
echo "Not enough events to synthesize a brief (fewer than ${min_events} in the lookback window). Use the ecosystem long enough to accumulate telemetry, then try again."
|
|
70
|
+
exit 0
|
|
71
|
+
elif [[ "$_rc" -ne 0 || -z "$OUTPUT_PATH" || ! -f "$OUTPUT_PATH" ]]; then
|
|
72
|
+
echo "Counsel synthesis failed. Check that the \`claude\` CLI is on PATH and the onlooker log is readable."
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then render the brief verbatim to the conversation:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
echo "## Counsel weekly review (\`$(basename "$OUTPUT_PATH" .md)\`)"
|
|
81
|
+
echo ""
|
|
82
|
+
cat "$OUTPUT_PATH"
|
|
83
|
+
echo ""
|
|
84
|
+
echo "_Brief saved to ${OUTPUT_PATH}._"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `/counsel --show` — print the latest brief (no LLM call)
|
|
88
|
+
|
|
89
|
+
Renders the most recent brief already on disk. No synthesis, no events emitted.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
LATEST=$(ls -1 "$BRIEFS_DIR"/*.md 2>/dev/null | sort | tail -1)
|
|
93
|
+
if [[ -z "$LATEST" || ! -f "$LATEST" ]]; then
|
|
94
|
+
echo "No brief on disk yet for this project. Run \`/counsel\` to generate one."
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
echo "## Counsel brief (\`$(basename "$LATEST" .md)\`)"
|
|
98
|
+
echo ""
|
|
99
|
+
cat "$LATEST"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `/counsel --status` — brief freshness
|
|
103
|
+
|
|
104
|
+
Reports the latest brief's age, last-generated time, and path. No LLM call.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
INTERVAL=$(counsel_config_get '.counsel.synthesis_interval_days'); INTERVAL="${INTERVAL:-7}"
|
|
108
|
+
LATEST=$(ls -1 "$BRIEFS_DIR"/*.md 2>/dev/null | sort | tail -1)
|
|
109
|
+
|
|
110
|
+
echo "## Counsel status"
|
|
111
|
+
echo "- Project key: ${PROJECT_KEY}"
|
|
112
|
+
echo "- Briefs dir: ${BRIEFS_DIR}"
|
|
113
|
+
echo "- Stale after: ${INTERVAL} days"
|
|
114
|
+
|
|
115
|
+
if [[ -z "$LATEST" || ! -f "$LATEST" ]]; then
|
|
116
|
+
echo "- Latest brief: none yet (run \`/counsel\`)"
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
121
|
+
mtime=$(stat -f '%m' "$LATEST" 2>/dev/null || echo 0)
|
|
122
|
+
human=$(date -r "$mtime" 2>/dev/null || echo "$mtime")
|
|
123
|
+
else
|
|
124
|
+
mtime=$(stat -c '%Y' "$LATEST" 2>/dev/null || echo 0)
|
|
125
|
+
human=$(date -d "@$mtime" 2>/dev/null || echo "$mtime")
|
|
126
|
+
fi
|
|
127
|
+
now=$(date +%s 2>/dev/null || echo "$mtime")
|
|
128
|
+
age_days=$(( (now - mtime) / 86400 ))
|
|
129
|
+
|
|
130
|
+
echo "- Latest brief: $(basename "$LATEST") (generated ${human}, ${age_days}d ago)"
|
|
131
|
+
if [[ "$age_days" -ge "$INTERVAL" ]]; then
|
|
132
|
+
echo "- Status: stale — SessionStart will regenerate, or run \`/counsel\` now."
|
|
133
|
+
else
|
|
134
|
+
echo "- Status: fresh — run \`/counsel\` to force a regeneration anyway."
|
|
135
|
+
fi
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Notes
|
|
139
|
+
|
|
140
|
+
- The synthesis pass shells out to `claude -p` with the configured
|
|
141
|
+
`evaluator.model` (default Haiku) — see the plugin README for all config keys.
|
|
142
|
+
- The on-demand path honors the same `capture.min_events` floor as the hook: if
|
|
143
|
+
too few events fall inside the lookback window, it reports that instead of
|
|
144
|
+
emitting a thin brief.
|
|
145
|
+
- Output, events, storage layout, and project keying are identical to the
|
|
146
|
+
SessionStart path — this skill only bypasses the staleness gate.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises counsel_generate_brief's staleness gate and the force bypass the
|
|
4
|
+
# on-demand /counsel skill relies on. The claude CLI is stubbed so synthesis is
|
|
5
|
+
# deterministic and offline.
|
|
6
|
+
|
|
7
|
+
setup() {
|
|
8
|
+
# shellcheck source=../helpers/setup.bash
|
|
9
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
10
|
+
setup_test_env
|
|
11
|
+
|
|
12
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/counsel"
|
|
13
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
14
|
+
# shellcheck disable=SC1091
|
|
15
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-config.sh"
|
|
16
|
+
# shellcheck disable=SC1091
|
|
17
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-events.sh"
|
|
18
|
+
# shellcheck disable=SC1091
|
|
19
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-project-key.sh"
|
|
20
|
+
# shellcheck disable=SC1091
|
|
21
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-ulid.sh"
|
|
22
|
+
# shellcheck disable=SC1091
|
|
23
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-reader.sh"
|
|
24
|
+
# shellcheck disable=SC1091
|
|
25
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-synthesize.sh"
|
|
26
|
+
# shellcheck disable=SC1091
|
|
27
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-brief.sh"
|
|
28
|
+
|
|
29
|
+
# A git work tree so counsel_project_key resolves to a stable root key.
|
|
30
|
+
WORK="${BATS_TEST_TMPDIR}/repo"
|
|
31
|
+
mkdir -p "$WORK"
|
|
32
|
+
git -C "$WORK" init -q
|
|
33
|
+
git -C "$WORK" config user.email test@example.com
|
|
34
|
+
git -C "$WORK" config user.name test
|
|
35
|
+
|
|
36
|
+
counsel_config_load "$WORK"
|
|
37
|
+
|
|
38
|
+
# Stub the claude CLI: ignore stdin/args, emit a valid synthesis object whose
|
|
39
|
+
# summary carries a marker we can assert on in the written brief.
|
|
40
|
+
STUB_BIN="${BATS_TEST_TMPDIR}/bin"
|
|
41
|
+
mkdir -p "$STUB_BIN"
|
|
42
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
43
|
+
#!/usr/bin/env bash
|
|
44
|
+
cat <<'JSON'
|
|
45
|
+
{"summary":"SYNTH_MARKER weekly review","patterns":["pattern one"],"recommendations":[{"title":"do x","rationale":"because y","priority":"high"}],"wins":["win one"],"watch":["watch one"]}
|
|
46
|
+
JSON
|
|
47
|
+
STUB
|
|
48
|
+
chmod +x "${STUB_BIN}/claude"
|
|
49
|
+
export PATH="${STUB_BIN}:${PATH}"
|
|
50
|
+
|
|
51
|
+
# Event log with comfortably more than min_events records in-window.
|
|
52
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
53
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
54
|
+
local ts
|
|
55
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || ts="2099-01-01T00:00:00Z"
|
|
56
|
+
: > "$ONLOOKER_EVENTS_LOG"
|
|
57
|
+
local i
|
|
58
|
+
for ((i = 0; i < 12; i++)); do
|
|
59
|
+
printf '%s\n' \
|
|
60
|
+
"{\"event_type\":\"tribunal.gate.blocked\",\"timestamp\":\"${ts}\",\"session_id\":\"s${i}\",\"payload\":{}}" \
|
|
61
|
+
>> "$ONLOOKER_EVENTS_LOG"
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
PROJECT_KEY=$(counsel_project_key "$WORK")
|
|
65
|
+
BRIEFS_DIR=$(counsel_project_dir "$PROJECT_KEY")
|
|
66
|
+
mkdir -p "$BRIEFS_DIR"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Staleness gate (default, non-force path used by the SessionStart hook)
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
# counsel_generate_brief prints the brief path on stdout and diagnostics on
|
|
74
|
+
# stderr; capture stdout only so $out is exactly the path.
|
|
75
|
+
gen() {
|
|
76
|
+
GEN_STATUS=0
|
|
77
|
+
GEN_OUT=$(counsel_generate_brief "$@" 2>/dev/null) || GEN_STATUS=$?
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@test "generate_brief writes a brief when none exists yet" {
|
|
81
|
+
gen "sess-1" "$WORK"
|
|
82
|
+
[ "$GEN_STATUS" -eq 0 ]
|
|
83
|
+
[ -f "$GEN_OUT" ]
|
|
84
|
+
grep -q "SYNTH_MARKER" "$GEN_OUT"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@test "generate_brief skips (rc=2) when the latest brief is still fresh" {
|
|
88
|
+
# A freshly written brief makes counsel_brief_is_stale return false.
|
|
89
|
+
printf '# old brief\n' > "${BRIEFS_DIR}/2099-01.md"
|
|
90
|
+
gen "sess-2" "$WORK"
|
|
91
|
+
[ "$GEN_STATUS" -eq 2 ]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Bullet lists and the rule in the rendered brief must not be mangled by
|
|
95
|
+
# printf treating a leading dash as an option.
|
|
96
|
+
@test "rendered brief contains intact bullets and horizontal rule" {
|
|
97
|
+
gen "sess-bullets" "$WORK"
|
|
98
|
+
[ "$GEN_STATUS" -eq 0 ]
|
|
99
|
+
grep -q '^- pattern one$' "$GEN_OUT"
|
|
100
|
+
grep -q '^- win one$' "$GEN_OUT"
|
|
101
|
+
grep -q '^---$' "$GEN_OUT"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Force bypass (on-demand /counsel skill path)
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
@test "force bypasses the staleness gate and regenerates a fresh brief" {
|
|
109
|
+
printf '# stale-looking but fresh-on-disk brief\n' > "${BRIEFS_DIR}/2099-01.md"
|
|
110
|
+
|
|
111
|
+
gen "sess-3" "$WORK" force
|
|
112
|
+
[ "$GEN_STATUS" -eq 0 ]
|
|
113
|
+
[ -f "$GEN_OUT" ]
|
|
114
|
+
grep -q "SYNTH_MARKER" "$GEN_OUT"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@test "force accepts the literal \"1\" as well" {
|
|
118
|
+
printf '# fresh\n' > "${BRIEFS_DIR}/2099-01.md"
|
|
119
|
+
gen "sess-4" "$WORK" 1
|
|
120
|
+
[ "$GEN_STATUS" -eq 0 ]
|
|
121
|
+
grep -q "SYNTH_MARKER" "$GEN_OUT"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@test "force still respects the min_events floor" {
|
|
125
|
+
# Only a couple of events — below the default min_events of 10.
|
|
126
|
+
local ts
|
|
127
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || ts="2099-01-01T00:00:00Z"
|
|
128
|
+
printf '%s\n' \
|
|
129
|
+
"{\"event_type\":\"tribunal.gate.blocked\",\"timestamp\":\"${ts}\",\"session_id\":\"s1\",\"payload\":{}}" \
|
|
130
|
+
"{\"event_type\":\"echo.regression.detected\",\"timestamp\":\"${ts}\",\"session_id\":\"s2\",\"payload\":{}}" \
|
|
131
|
+
> "$ONLOOKER_EVENTS_LOG"
|
|
132
|
+
|
|
133
|
+
gen "sess-5" "$WORK" force
|
|
134
|
+
[ "$GEN_STATUS" -eq 2 ]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Regression: counsel.brief.generated must validate against the schema and land
|
|
138
|
+
# in the event log. The period bounds are emitted as RFC 3339 date-time strings.
|
|
139
|
+
@test "generated brief emits a schema-valid counsel.brief.generated event" {
|
|
140
|
+
gen "sess-evt" "$WORK"
|
|
141
|
+
[ "$GEN_STATUS" -eq 0 ]
|
|
142
|
+
run grep -c '"event_type":"counsel.brief.generated"' "$ONLOOKER_EVENTS_LOG"
|
|
143
|
+
[ "$status" -eq 0 ]
|
|
144
|
+
[ "$output" -ge 1 ]
|
|
145
|
+
# period_start must be a full date-time, not a bare calendar date.
|
|
146
|
+
run grep -o '"period_start":"[^"]*"' "$ONLOOKER_EVENTS_LOG"
|
|
147
|
+
[[ "$output" == *"T"*"Z"* ]]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# If date cannot produce timestamps the bounds are empty; rather than emit an
|
|
151
|
+
# event that fails schema validation, the emit is skipped and the brief is
|
|
152
|
+
# still written.
|
|
153
|
+
@test "emit is skipped (never invalid) when date cannot produce bounds" {
|
|
154
|
+
local datestub="${BATS_TEST_TMPDIR}/datebin"
|
|
155
|
+
mkdir -p "$datestub"
|
|
156
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "${datestub}/date"
|
|
157
|
+
chmod +x "${datestub}/date"
|
|
158
|
+
PATH="${datestub}:${PATH}"
|
|
159
|
+
|
|
160
|
+
gen "sess-nodate" "$WORK" force
|
|
161
|
+
[ "$GEN_STATUS" -eq 0 ]
|
|
162
|
+
[ -f "$GEN_OUT" ]
|
|
163
|
+
grep -q "SYNTH_MARKER" "$GEN_OUT"
|
|
164
|
+
# No counsel.brief.generated event should have been appended.
|
|
165
|
+
run grep -c '"event_type":"counsel.brief.generated"' "$ONLOOKER_EVENTS_LOG"
|
|
166
|
+
[ "$output" -eq 0 ]
|
|
167
|
+
}
|