@onlooker-community/ecosystem 0.17.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 +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +3 -2
- package/CHANGELOG.md +7 -0
- package/package.json +2 -2
- 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/release-please-config.json +16 -0
- package/test/bats/counsel-project-key.bats +82 -0
- package/test/bats/counsel-reader.bats +132 -0
|
@@ -98,6 +98,19 @@
|
|
|
98
98
|
"license": "MIT",
|
|
99
99
|
"keywords": ["documentation", "intent", "decisions", "tradeoffs", "why", "context"],
|
|
100
100
|
"tags": ["documentation", "memory"]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "counsel",
|
|
104
|
+
"source": "./plugins/counsel",
|
|
105
|
+
"description": "Weekly synthesis and recommendations from your full observability stack. Reads all plugin event logs, identifies patterns, surfaces improvement opportunities, and injects a structured brief at session start when the last brief is stale. Turns disparate logs into a coaching signal. Requires the ecosystem plugin.",
|
|
106
|
+
"author": {
|
|
107
|
+
"name": "Onlooker Community"
|
|
108
|
+
},
|
|
109
|
+
"homepage": "https://onlooker.dev",
|
|
110
|
+
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
111
|
+
"license": "MIT",
|
|
112
|
+
"keywords": ["synthesis", "recommendations", "observability", "coaching", "patterns", "weekly"],
|
|
113
|
+
"tags": ["observability", "coaching"]
|
|
101
114
|
}
|
|
102
115
|
]
|
|
103
116
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Observability substrate for Claude Code. Provides the shared ~/.onlooker/ storage root, 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,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
".": "0.
|
|
2
|
+
".": "0.18.0",
|
|
3
3
|
"plugins/archivist": "0.1.0",
|
|
4
4
|
"plugins/tribunal": "1.0.1",
|
|
5
5
|
"plugins/echo": "0.2.0",
|
|
6
6
|
"plugins/cartographer": "0.2.0",
|
|
7
7
|
"plugins/governor": "0.2.0",
|
|
8
8
|
"plugins/compass": "0.2.0",
|
|
9
|
-
"plugins/scribe": "0.2.0"
|
|
9
|
+
"plugins/scribe": "0.2.0",
|
|
10
|
+
"plugins/counsel": "0.2.0"
|
|
10
11
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.17.0...ecosystem-v0.18.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))
|
|
9
|
+
|
|
3
10
|
## [0.17.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.16.0...ecosystem-v0.17.0) (2026-06-01)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlooker-community/ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Agents, skills, hooks, commands, rules, and MCP configurations that power [Onlooker](https://onlooker.dev)",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"test": "npm run test:bats && npm run test:schema",
|
|
27
27
|
"test:bats": "bats test/bats",
|
|
28
28
|
"test:schema": "node --test test/node/*.test.mjs",
|
|
29
|
-
"test:shellcheck": "shellcheck -S error -x install.sh scripts/common.sh scripts/hooks/*.sh scripts/lib/*.sh plugins/archivist/scripts/hooks/*.sh plugins/archivist/scripts/lib/*.sh plugins/tribunal/scripts/hooks/*.sh plugins/tribunal/scripts/lib/*.sh plugins/echo/scripts/hooks/*.sh plugins/echo/scripts/lib/*.sh plugins/governor/scripts/hooks/*.sh plugins/governor/scripts/lib/*.sh plugins/compass/scripts/hooks/*.sh plugins/compass/scripts/lib/*.sh plugins/scribe/scripts/hooks/*.sh plugins/scribe/scripts/lib/*.sh",
|
|
29
|
+
"test:shellcheck": "shellcheck -S error -x install.sh scripts/common.sh scripts/hooks/*.sh scripts/lib/*.sh plugins/archivist/scripts/hooks/*.sh plugins/archivist/scripts/lib/*.sh plugins/tribunal/scripts/hooks/*.sh plugins/tribunal/scripts/lib/*.sh plugins/echo/scripts/hooks/*.sh plugins/echo/scripts/lib/*.sh plugins/governor/scripts/hooks/*.sh plugins/governor/scripts/lib/*.sh plugins/compass/scripts/hooks/*.sh plugins/compass/scripts/lib/*.sh plugins/scribe/scripts/hooks/*.sh plugins/scribe/scripts/lib/*.sh plugins/counsel/scripts/hooks/*.sh plugins/counsel/scripts/lib/*.sh",
|
|
30
30
|
"lint:references": "node scripts/lint/check-references.mjs",
|
|
31
31
|
"lint:manifests": "node scripts/lint/check-manifests.mjs",
|
|
32
32
|
"coverage:node": "node scripts/coverage/run-coverage.mjs",
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Event log reader for Counsel.
|
|
3
|
+
#
|
|
4
|
+
# Reads $ONLOOKER_EVENTS_LOG and returns a filtered, summarized view of
|
|
5
|
+
# the last N days suitable for passing to the synthesis prompt.
|
|
6
|
+
#
|
|
7
|
+
# Exposes:
|
|
8
|
+
# counsel_read_events <lookback_days> <chars_max>
|
|
9
|
+
# Echoes a structured text summary of events, or empty string on failure.
|
|
10
|
+
#
|
|
11
|
+
# counsel_sources_from_events <events_json>
|
|
12
|
+
# Echoes a JSON array of CounselSource strings present in the event batch.
|
|
13
|
+
|
|
14
|
+
# Maps event_type prefixes to CounselSource values.
|
|
15
|
+
_counsel_source_for_type() {
|
|
16
|
+
local event_type="${1:-}"
|
|
17
|
+
case "$event_type" in
|
|
18
|
+
tribunal.*) printf 'tribunal_verdicts' ;;
|
|
19
|
+
echo.*) printf 'echo_regressions' ;;
|
|
20
|
+
sentinel.*) printf 'sentinel_audit' ;;
|
|
21
|
+
warden.*) printf 'warden_audit' ;;
|
|
22
|
+
oracle.*) printf 'oracle_calibrations' ;;
|
|
23
|
+
meridian.*) printf 'meridian_reliance' ;;
|
|
24
|
+
*) printf 'onlooker_events' ;;
|
|
25
|
+
esac
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
counsel_read_events() {
|
|
29
|
+
local lookback_days="${1:-30}"
|
|
30
|
+
local chars_max="${2:-60000}"
|
|
31
|
+
|
|
32
|
+
local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
|
|
33
|
+
[[ -f "$log_path" ]] || { printf ''; return 0; }
|
|
34
|
+
|
|
35
|
+
# Compute cutoff as an ISO 8601 date string. ISO 8601 strings are
|
|
36
|
+
# lexicographically sortable, so string comparison is safe for filtering.
|
|
37
|
+
local cutoff_ts
|
|
38
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
39
|
+
cutoff_ts=$(date -v "-${lookback_days}d" -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
40
|
+
|| cutoff_ts=""
|
|
41
|
+
else
|
|
42
|
+
cutoff_ts=$(date -d "-${lookback_days} days" -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
|
|
43
|
+
|| cutoff_ts=""
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Filter to events within the lookback window. If cutoff_ts is empty (date
|
|
47
|
+
# command unavailable) fall through and include all events.
|
|
48
|
+
local summary
|
|
49
|
+
# -rc: compact output keeps each object on one line (JSONL-shaped), which
|
|
50
|
+
# downstream counsel_count_events and counsel_sources_from_events require.
|
|
51
|
+
summary=$(jq -rc --arg cutoff "$cutoff_ts" '
|
|
52
|
+
select(.timestamp != null) |
|
|
53
|
+
select($cutoff == "" or .timestamp >= $cutoff) |
|
|
54
|
+
{
|
|
55
|
+
type: .event_type,
|
|
56
|
+
plugin: (.plugin // "unknown"),
|
|
57
|
+
ts: .timestamp,
|
|
58
|
+
session: (.session_id // ""),
|
|
59
|
+
payload: (.payload // {})
|
|
60
|
+
}
|
|
61
|
+
' "$log_path" 2>/dev/null | head -c "$chars_max") || summary=""
|
|
62
|
+
|
|
63
|
+
printf '%s' "$summary"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
counsel_sources_from_events() {
|
|
67
|
+
local events_text="${1:-}"
|
|
68
|
+
[[ -z "$events_text" ]] && { printf '["onlooker_events"]'; return 0; }
|
|
69
|
+
|
|
70
|
+
local sources=()
|
|
71
|
+
local seen_tribunal=0 seen_echo=0 seen_sentinel=0 seen_warden=0 seen_oracle=0 seen_meridian=0 seen_other=0
|
|
72
|
+
|
|
73
|
+
while IFS= read -r line; do
|
|
74
|
+
[[ -z "$line" ]] && continue
|
|
75
|
+
local etype
|
|
76
|
+
etype=$(printf '%s' "$line" | jq -r '.type // ""' 2>/dev/null) || continue
|
|
77
|
+
case "$etype" in
|
|
78
|
+
tribunal.*) seen_tribunal=1 ;;
|
|
79
|
+
echo.*) seen_echo=1 ;;
|
|
80
|
+
sentinel.*) seen_sentinel=1 ;;
|
|
81
|
+
warden.*) seen_warden=1 ;;
|
|
82
|
+
oracle.*) seen_oracle=1 ;;
|
|
83
|
+
meridian.*) seen_meridian=1 ;;
|
|
84
|
+
*) seen_other=1 ;;
|
|
85
|
+
esac
|
|
86
|
+
done <<< "$events_text"
|
|
87
|
+
|
|
88
|
+
[[ "$seen_other" -eq 1 ]] && sources+=("\"onlooker_events\"")
|
|
89
|
+
[[ "$seen_tribunal" -eq 1 ]] && sources+=("\"tribunal_verdicts\"")
|
|
90
|
+
[[ "$seen_echo" -eq 1 ]] && sources+=("\"echo_regressions\"")
|
|
91
|
+
[[ "$seen_sentinel" -eq 1 ]] && sources+=("\"sentinel_audit\"")
|
|
92
|
+
[[ "$seen_warden" -eq 1 ]] && sources+=("\"warden_audit\"")
|
|
93
|
+
[[ "$seen_oracle" -eq 1 ]] && sources+=("\"oracle_calibrations\"")
|
|
94
|
+
[[ "$seen_meridian" -eq 1 ]] && sources+=("\"meridian_reliance\"")
|
|
95
|
+
|
|
96
|
+
if [[ "${#sources[@]}" -eq 0 ]]; then
|
|
97
|
+
printf '["onlooker_events"]'
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
local joined
|
|
102
|
+
joined=$(IFS=,; printf '%s' "${sources[*]}")
|
|
103
|
+
printf '[%s]' "$joined"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
counsel_count_events() {
|
|
107
|
+
local events_text="${1:-}"
|
|
108
|
+
[[ -z "$events_text" ]] && { printf '0'; return 0; }
|
|
109
|
+
local count=0
|
|
110
|
+
while IFS= read -r line; do
|
|
111
|
+
[[ -n "$line" ]] && count=$((count + 1))
|
|
112
|
+
done <<< "$events_text"
|
|
113
|
+
printf '%s' "$count"
|
|
114
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Synthesis pass for Counsel.
|
|
3
|
+
#
|
|
4
|
+
# Runs a single Haiku call over the event summary to produce a structured
|
|
5
|
+
# improvement brief. The brief is returned as a JSON object.
|
|
6
|
+
#
|
|
7
|
+
# Exposes:
|
|
8
|
+
# counsel_synthesize <events_text> <model> <timeout_s> <max_tokens> <temperature>
|
|
9
|
+
# Echoes a JSON object on success, empty string on failure.
|
|
10
|
+
# JSON shape:
|
|
11
|
+
# {
|
|
12
|
+
# "summary": string,
|
|
13
|
+
# "patterns": [string],
|
|
14
|
+
# "recommendations": [{title, rationale, priority:"high"|"medium"|"low"}],
|
|
15
|
+
# "wins": [string],
|
|
16
|
+
# "watch": [string]
|
|
17
|
+
# }
|
|
18
|
+
|
|
19
|
+
_COUNSEL_SYNTHESIS_PROMPT='You are an engineering coach analyzing an AI agent observability log. You have been given a structured dump of plugin events from the onlooker ecosystem over the past several weeks. Your job is to synthesize patterns, surface improvement opportunities, and highlight what is working well.
|
|
20
|
+
|
|
21
|
+
Focus on:
|
|
22
|
+
- Recurring failure modes or blocked gates (tribunal, sentinel, warden)
|
|
23
|
+
- Prompt regression trends (echo plugin)
|
|
24
|
+
- Budget or resource pressure patterns (governor plugin)
|
|
25
|
+
- Quality trends over time
|
|
26
|
+
- What the team is consistently doing well
|
|
27
|
+
|
|
28
|
+
Return a JSON object with exactly these keys:
|
|
29
|
+
{
|
|
30
|
+
"summary": "2-3 sentence executive summary of the period",
|
|
31
|
+
"patterns": ["observed pattern — what is happening and how often"],
|
|
32
|
+
"recommendations": [
|
|
33
|
+
{
|
|
34
|
+
"title": "short action title",
|
|
35
|
+
"rationale": "1-2 sentences explaining why this matters",
|
|
36
|
+
"priority": "high"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"wins": ["thing that is working well — be specific"],
|
|
40
|
+
"watch": ["trend to monitor — not urgent but worth watching"]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
- All fields are required; use empty arrays [] if no items found
|
|
45
|
+
- recommendations must have priority: "high", "medium", or "low"
|
|
46
|
+
- Keep each item to 1-2 sentences
|
|
47
|
+
- Return ONLY the JSON object — no prose, no markdown fences, no explanation
|
|
48
|
+
- If there is insufficient data to draw conclusions, say so in summary and return empty arrays
|
|
49
|
+
|
|
50
|
+
'
|
|
51
|
+
|
|
52
|
+
counsel_synthesize() {
|
|
53
|
+
local events_text="${1:-}"
|
|
54
|
+
local model="${2:-claude-haiku-4-5-20251001}"
|
|
55
|
+
local timeout_s="${3:-90}"
|
|
56
|
+
local max_tokens="${4:-4096}"
|
|
57
|
+
local temperature="${5:-0.4}"
|
|
58
|
+
|
|
59
|
+
[[ -z "$events_text" ]] && return 1
|
|
60
|
+
|
|
61
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
62
|
+
printf 'counsel_synthesize: claude CLI not found\n' >&2
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
local prompt_file
|
|
67
|
+
prompt_file=$(mktemp -t counsel-synth.XXXXXX 2>/dev/null) || prompt_file="/tmp/counsel-synth.$$"
|
|
68
|
+
trap 'rm -f "$prompt_file"' RETURN
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
printf '%s' "$_COUNSEL_SYNTHESIS_PROMPT"
|
|
72
|
+
printf '<event_log>\n'
|
|
73
|
+
printf '%s\n' "$events_text"
|
|
74
|
+
printf '</event_log>\n'
|
|
75
|
+
} > "$prompt_file"
|
|
76
|
+
|
|
77
|
+
local claude_args=(-p --max-turns 1 --model "$model" --max-tokens "$max_tokens")
|
|
78
|
+
|
|
79
|
+
local response=""
|
|
80
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
81
|
+
response=$(timeout "$timeout_s" claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
|
|
82
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
83
|
+
response=$(gtimeout "$timeout_s" claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
|
|
84
|
+
else
|
|
85
|
+
response=$(claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
[[ -z "$response" ]] && return 1
|
|
89
|
+
|
|
90
|
+
# Strip markdown fences if present.
|
|
91
|
+
local clean
|
|
92
|
+
clean=$(printf '%s' "$response" \
|
|
93
|
+
| sed -e 's/^```json[[:space:]]*//' -e 's/^```[[:space:]]*//' -e 's/[[:space:]]*```$//')
|
|
94
|
+
|
|
95
|
+
if ! printf '%s' "$clean" | jq -e \
|
|
96
|
+
'.summary and (.patterns | type == "array") and (.recommendations | type == "array") and (.wins | type == "array") and (.watch | type == "array")' \
|
|
97
|
+
>/dev/null 2>&1; then
|
|
98
|
+
printf 'counsel_synthesize: response missing required keys\n' >&2
|
|
99
|
+
return 1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
printf '%s' "$clean"
|
|
103
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Minimal ULID generator for Counsel brief IDs.
|
|
3
|
+
|
|
4
|
+
_COUNSEL_ULID_ALPHABET="0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
5
|
+
|
|
6
|
+
_counsel_ulid_encode() {
|
|
7
|
+
local n="$1"
|
|
8
|
+
local len="$2"
|
|
9
|
+
local out=""
|
|
10
|
+
local i
|
|
11
|
+
for ((i = 0; i < len; i++)); do
|
|
12
|
+
out="${_COUNSEL_ULID_ALPHABET:$((n % 32)):1}${out}"
|
|
13
|
+
n=$((n / 32))
|
|
14
|
+
done
|
|
15
|
+
printf '%s' "$out"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
counsel_ulid() {
|
|
19
|
+
local now_ms
|
|
20
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
21
|
+
now_ms=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null) \
|
|
22
|
+
|| now_ms=$(($(date +%s) * 1000))
|
|
23
|
+
else
|
|
24
|
+
now_ms=$(date +%s%3N 2>/dev/null) || now_ms=$(($(date +%s) * 1000))
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
local rand_hex rand_hi rand_lo
|
|
28
|
+
rand_hex=$(openssl rand -hex 10 2>/dev/null)
|
|
29
|
+
if [[ -n "$rand_hex" && ${#rand_hex} -eq 20 ]]; then
|
|
30
|
+
rand_hi=$((16#${rand_hex:0:10}))
|
|
31
|
+
rand_lo=$((16#${rand_hex:10:10}))
|
|
32
|
+
else
|
|
33
|
+
rand_hi=$((RANDOM * 32768 + RANDOM))
|
|
34
|
+
rand_lo=$((RANDOM * 32768 + RANDOM))
|
|
35
|
+
rand_hi=$(((rand_hi * 256 + RANDOM % 256) & ((1 << 40) - 1)))
|
|
36
|
+
rand_lo=$(((rand_lo * 256 + RANDOM % 256) & ((1 << 40) - 1)))
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
local ts_part hi_part lo_part
|
|
40
|
+
ts_part=$(_counsel_ulid_encode "$now_ms" 10)
|
|
41
|
+
hi_part=$(_counsel_ulid_encode "$rand_hi" 8)
|
|
42
|
+
lo_part=$(_counsel_ulid_encode "$rand_lo" 8)
|
|
43
|
+
|
|
44
|
+
printf '%s%s%s' "$ts_part" "$hi_part" "$lo_part"
|
|
45
|
+
}
|
|
@@ -126,6 +126,22 @@
|
|
|
126
126
|
"jsonpath": "$.version"
|
|
127
127
|
}
|
|
128
128
|
]
|
|
129
|
+
},
|
|
130
|
+
"plugins/counsel": {
|
|
131
|
+
"changelog-path": "CHANGELOG.md",
|
|
132
|
+
"release-type": "simple",
|
|
133
|
+
"bump-minor-pre-major": true,
|
|
134
|
+
"bump-patch-for-minor-pre-major": false,
|
|
135
|
+
"component": "counsel",
|
|
136
|
+
"draft": false,
|
|
137
|
+
"prerelease": false,
|
|
138
|
+
"extra-files": [
|
|
139
|
+
{
|
|
140
|
+
"type": "json",
|
|
141
|
+
"path": ".claude-plugin/plugin.json",
|
|
142
|
+
"jsonpath": "$.version"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
129
145
|
}
|
|
130
146
|
},
|
|
131
147
|
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
setup_test_env
|
|
7
|
+
|
|
8
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/counsel"
|
|
9
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-project-key.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@test "non-git directory returns empty key" {
|
|
15
|
+
local d="${BATS_TEST_TMPDIR}/non-git"
|
|
16
|
+
mkdir -p "$d"
|
|
17
|
+
run counsel_project_key "$d"
|
|
18
|
+
[ "$status" -eq 0 ]
|
|
19
|
+
[ -z "$output" ]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@test "git repo without remote falls back to repo-root hash" {
|
|
23
|
+
local d="${BATS_TEST_TMPDIR}/local-only-repo"
|
|
24
|
+
mkdir -p "$d"
|
|
25
|
+
git -C "$d" init -q
|
|
26
|
+
git -C "$d" config user.email t@example.com
|
|
27
|
+
git -C "$d" config user.name "Test"
|
|
28
|
+
|
|
29
|
+
local k1
|
|
30
|
+
k1=$(counsel_project_key "$d")
|
|
31
|
+
[ -n "$k1" ]
|
|
32
|
+
[ "${#k1}" -eq 12 ]
|
|
33
|
+
|
|
34
|
+
local k2
|
|
35
|
+
k2=$(counsel_project_key "$d")
|
|
36
|
+
[ "$k1" = "$k2" ]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test "git repo with remote uses remote hash, ignores local path" {
|
|
40
|
+
local a="${BATS_TEST_TMPDIR}/clone-a"
|
|
41
|
+
local b="${BATS_TEST_TMPDIR}/clone-b"
|
|
42
|
+
mkdir -p "$a" "$b"
|
|
43
|
+
for d in "$a" "$b"; do
|
|
44
|
+
git -C "$d" init -q
|
|
45
|
+
git -C "$d" config user.email t@example.com
|
|
46
|
+
git -C "$d" config user.name "Test"
|
|
47
|
+
git -C "$d" remote add origin git@github.com:org/proj.git
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
local ka kb
|
|
51
|
+
ka=$(counsel_project_key "$a")
|
|
52
|
+
kb=$(counsel_project_key "$b")
|
|
53
|
+
[ -n "$ka" ]
|
|
54
|
+
[ "$ka" = "$kb" ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "different remotes yield different keys" {
|
|
58
|
+
local a="${BATS_TEST_TMPDIR}/proj-a"
|
|
59
|
+
local b="${BATS_TEST_TMPDIR}/proj-b"
|
|
60
|
+
mkdir -p "$a" "$b"
|
|
61
|
+
for d in "$a" "$b"; do
|
|
62
|
+
git -C "$d" init -q
|
|
63
|
+
git -C "$d" config user.email t@example.com
|
|
64
|
+
git -C "$d" config user.name "Test"
|
|
65
|
+
done
|
|
66
|
+
git -C "$a" remote add origin git@github.com:org/proj-a.git
|
|
67
|
+
git -C "$b" remote add origin git@github.com:org/proj-b.git
|
|
68
|
+
|
|
69
|
+
local ka kb
|
|
70
|
+
ka=$(counsel_project_key "$a")
|
|
71
|
+
kb=$(counsel_project_key "$b")
|
|
72
|
+
[ -n "$ka" ]
|
|
73
|
+
[ -n "$kb" ]
|
|
74
|
+
[ "$ka" != "$kb" ]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@test "counsel_project_dir includes project key in path" {
|
|
78
|
+
local key="abc123def456"
|
|
79
|
+
run counsel_project_dir "$key"
|
|
80
|
+
[ "$status" -eq 0 ]
|
|
81
|
+
[[ "$output" == *"counsel/${key}/briefs" ]]
|
|
82
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
setup_test_env
|
|
7
|
+
|
|
8
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/counsel"
|
|
9
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/counsel-reader.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# counsel_count_events
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
@test "count_events returns 0 for empty input" {
|
|
19
|
+
run counsel_count_events ""
|
|
20
|
+
[ "$status" -eq 0 ]
|
|
21
|
+
[ "$output" = "0" ]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@test "count_events counts non-blank lines" {
|
|
25
|
+
local text
|
|
26
|
+
text=$(printf '%s\n%s\n%s\n' '{"type":"a"}' '{"type":"b"}' '{"type":"c"}')
|
|
27
|
+
run counsel_count_events "$text"
|
|
28
|
+
[ "$status" -eq 0 ]
|
|
29
|
+
[ "$output" = "3" ]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# counsel_sources_from_events
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
@test "sources_from_events returns onlooker_events for unknown types" {
|
|
37
|
+
local text='{"type":"scribe.distill.complete"}'
|
|
38
|
+
run counsel_sources_from_events "$text"
|
|
39
|
+
[ "$status" -eq 0 ]
|
|
40
|
+
[[ "$output" == *"onlooker_events"* ]]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "sources_from_events detects tribunal events" {
|
|
44
|
+
local text='{"type":"tribunal.gate.blocked"}'
|
|
45
|
+
run counsel_sources_from_events "$text"
|
|
46
|
+
[ "$status" -eq 0 ]
|
|
47
|
+
[[ "$output" == *"tribunal_verdicts"* ]]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@test "sources_from_events detects echo events" {
|
|
51
|
+
local text='{"type":"echo.regression.detected"}'
|
|
52
|
+
run counsel_sources_from_events "$text"
|
|
53
|
+
[ "$status" -eq 0 ]
|
|
54
|
+
[[ "$output" == *"echo_regressions"* ]]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "sources_from_events returns onlooker_events for empty input" {
|
|
58
|
+
run counsel_sources_from_events ""
|
|
59
|
+
[ "$status" -eq 0 ]
|
|
60
|
+
[ "$output" = '["onlooker_events"]' ]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# counsel_read_events — file-based
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
@test "read_events returns empty when log does not exist" {
|
|
68
|
+
export ONLOOKER_EVENTS_LOG="${BATS_TEST_TMPDIR}/no-log.jsonl"
|
|
69
|
+
run counsel_read_events "30" "60000"
|
|
70
|
+
[ "$status" -eq 0 ]
|
|
71
|
+
[ -z "$output" ]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@test "read_events returns empty for empty log" {
|
|
75
|
+
local log="${BATS_TEST_TMPDIR}/empty-log.jsonl"
|
|
76
|
+
touch "$log"
|
|
77
|
+
export ONLOOKER_EVENTS_LOG="$log"
|
|
78
|
+
run counsel_read_events "30" "60000"
|
|
79
|
+
[ "$status" -eq 0 ]
|
|
80
|
+
[ -z "$output" ]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@test "read_events filters events within lookback window" {
|
|
84
|
+
local log="${BATS_TEST_TMPDIR}/events.jsonl"
|
|
85
|
+
# Use a timestamp far in the future to ensure it passes any lookback filter.
|
|
86
|
+
local ts
|
|
87
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || ts="2099-01-01T00:00:00Z"
|
|
88
|
+
printf '%s\n' \
|
|
89
|
+
"{\"event_type\":\"scribe.distill.complete\",\"timestamp\":\"${ts}\",\"session_id\":\"s1\",\"payload\":{}}" \
|
|
90
|
+
> "$log"
|
|
91
|
+
export ONLOOKER_EVENTS_LOG="$log"
|
|
92
|
+
run counsel_read_events "30" "60000"
|
|
93
|
+
[ "$status" -eq 0 ]
|
|
94
|
+
[[ "$output" == *"scribe.distill.complete"* ]]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "read_events output is JSONL-shaped: one object per line" {
|
|
98
|
+
local log="${BATS_TEST_TMPDIR}/multi-events.jsonl"
|
|
99
|
+
local ts
|
|
100
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || ts="2099-01-01T00:00:00Z"
|
|
101
|
+
printf '%s\n' \
|
|
102
|
+
"{\"event_type\":\"tribunal.gate.blocked\",\"timestamp\":\"${ts}\",\"session_id\":\"s1\",\"payload\":{}}" \
|
|
103
|
+
"{\"event_type\":\"echo.regression.detected\",\"timestamp\":\"${ts}\",\"session_id\":\"s2\",\"payload\":{}}" \
|
|
104
|
+
"{\"event_type\":\"scribe.distill.complete\",\"timestamp\":\"${ts}\",\"session_id\":\"s3\",\"payload\":{}}" \
|
|
105
|
+
> "$log"
|
|
106
|
+
export ONLOOKER_EVENTS_LOG="$log"
|
|
107
|
+
|
|
108
|
+
local events
|
|
109
|
+
events=$(counsel_read_events "30" "60000")
|
|
110
|
+
|
|
111
|
+
# count_events must see exactly 3 records, not inflated by pretty-printing.
|
|
112
|
+
run counsel_count_events "$events"
|
|
113
|
+
[ "$status" -eq 0 ]
|
|
114
|
+
[ "$output" = "3" ]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@test "read_events output preserves source types for sources_from_events" {
|
|
118
|
+
local log="${BATS_TEST_TMPDIR}/source-events.jsonl"
|
|
119
|
+
local ts
|
|
120
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || ts="2099-01-01T00:00:00Z"
|
|
121
|
+
printf '%s\n' \
|
|
122
|
+
"{\"event_type\":\"tribunal.gate.blocked\",\"timestamp\":\"${ts}\",\"session_id\":\"s1\",\"payload\":{}}" \
|
|
123
|
+
> "$log"
|
|
124
|
+
export ONLOOKER_EVENTS_LOG="$log"
|
|
125
|
+
|
|
126
|
+
local events
|
|
127
|
+
events=$(counsel_read_events "30" "60000")
|
|
128
|
+
|
|
129
|
+
run counsel_sources_from_events "$events"
|
|
130
|
+
[ "$status" -eq 0 ]
|
|
131
|
+
[[ "$output" == *"tribunal_verdicts"* ]]
|
|
132
|
+
}
|