@onlooker-community/ecosystem 0.17.0 → 0.19.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 +26 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +4 -2
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +1 -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/plugins/warden/.claude-plugin/plugin.json +14 -0
- package/plugins/warden/CHANGELOG.md +10 -0
- package/plugins/warden/config.json +51 -0
- package/plugins/warden/docs/adr/001-detect-after-ingest-gate-before-action.md +62 -0
- package/plugins/warden/docs/design.md +123 -0
- package/plugins/warden/hooks/hooks.json +73 -0
- package/plugins/warden/scripts/hooks/warden-post-tool-use.sh +201 -0
- package/plugins/warden/scripts/hooks/warden-pre-tool-use.sh +94 -0
- package/plugins/warden/scripts/hooks/warden-session-start.sh +52 -0
- package/plugins/warden/scripts/lib/warden-cli.sh +124 -0
- package/plugins/warden/scripts/lib/warden-config.sh +79 -0
- package/plugins/warden/scripts/lib/warden-evaluator.sh +246 -0
- package/plugins/warden/scripts/lib/warden-events.sh +85 -0
- package/plugins/warden/scripts/lib/warden-gate-state.sh +105 -0
- package/plugins/warden/scripts/lib/warden-patterns.sh +132 -0
- package/plugins/warden/scripts/lib/warden-sanitizer.sh +80 -0
- package/plugins/warden/scripts/lib/warden-scanner.sh +119 -0
- package/plugins/warden/scripts/lib/warden-ulid.sh +50 -0
- package/plugins/warden/skills/warden/SKILL.md +49 -0
- package/release-please-config.json +32 -0
- package/test/bats/counsel-project-key.bats +82 -0
- package/test/bats/counsel-reader.bats +132 -0
- package/test/bats/warden-config.bats +54 -0
- package/test/bats/warden-events.bats +85 -0
- package/test/bats/warden-gate-state.bats +67 -0
- package/test/bats/warden-patterns.bats +58 -0
- package/test/bats/warden-sanitizer.bats +53 -0
- package/test/bats/warden-scanner.bats +56 -0
- package/test/bats/warden-ulid.bats +30 -0
|
@@ -98,6 +98,32 @@
|
|
|
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"]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "warden",
|
|
117
|
+
"source": "./plugins/warden",
|
|
118
|
+
"description": "Untrusted-content gate. Scans content flowing in through WebFetch and Read for prompt-injection patterns, and when a threat is detected closes a session-scoped gate that blocks Write, Edit, and Bash until the user explicitly clears it. Grounded in Meta's Agents Rule of Two — warden removes the agent's external-actions property while untrusted content is in play. Requires the ecosystem plugin.",
|
|
119
|
+
"author": {
|
|
120
|
+
"name": "Onlooker Community"
|
|
121
|
+
},
|
|
122
|
+
"homepage": "https://onlooker.dev",
|
|
123
|
+
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
124
|
+
"license": "MIT",
|
|
125
|
+
"keywords": ["security", "prompt-injection", "rule-of-two", "safety", "content-gate", "untrusted-content"],
|
|
126
|
+
"tags": ["safety", "security"]
|
|
101
127
|
}
|
|
102
128
|
]
|
|
103
129
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
".": "0.
|
|
2
|
+
".": "0.19.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",
|
|
11
|
+
"plugins/warden": "0.2.0"
|
|
10
12
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.18.0...ecosystem-v0.19.0) (2026-06-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **warden:** untrusted-content gate enforcing the Agents Rule of Two :shield: ([#53](https://github.com/onlooker-community/ecosystem/issues/53)) ([210aa51](https://github.com/onlooker-community/ecosystem/commit/210aa51bff66226a0eec1f17292a2af4ea4ef56a))
|
|
9
|
+
|
|
10
|
+
## [0.18.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.17.0...ecosystem-v0.18.0) (2026-06-02)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **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))
|
|
16
|
+
|
|
3
17
|
## [0.17.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.16.0...ecosystem-v0.17.0) (2026-06-01)
|
|
4
18
|
|
|
5
19
|
|
package/CLAUDE.md
CHANGED
|
@@ -36,6 +36,7 @@ scripts/lib/onlooker-event.mjs ← canonical event builder; all plugins route t
|
|
|
36
36
|
| echo | Stop | Regression-tests prompt changes after each agent stop |
|
|
37
37
|
| governor | SessionStart, PreToolUse (Task), PostToolUse (Task), Stop | Budget gates on subagent spawns; tracks spend per session |
|
|
38
38
|
| tribunal | Stop + skill invocation | Post-task quality gate; also invokable via `/tribunal` |
|
|
39
|
+
| warden | PostToolUse (WebFetch, Read), PreToolUse (Write, Edit, MultiEdit, Bash), SessionStart + skill invocation | Scans ingested content for injection; closes a content gate that blocks write-class tools until cleared via `/warden` |
|
|
39
40
|
|
|
40
41
|
Plugins communicate by emitting events to the JSONL log — they do not call each other directly. All plugins depend on the ecosystem substrate; no plugin depends on another plugin directly.
|
|
41
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlooker-community/ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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 plugins/warden/scripts/hooks/*.sh plugins/warden/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
|
+
}
|