@onlooker-community/ecosystem 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +39 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +5 -2
- package/CHANGELOG.md +15 -0
- package/CLAUDE.md +88 -0
- package/package.json +3 -3
- package/plugins/compass/.claude-plugin/plugin.json +14 -0
- package/plugins/compass/CHANGELOG.md +8 -0
- package/plugins/compass/config.json +71 -0
- package/plugins/compass/docs/adr/001-evaluate-prompts-in-context.md +82 -0
- package/plugins/compass/docs/design.md +421 -0
- package/plugins/compass/hooks/hooks.json +82 -0
- package/plugins/compass/scripts/hooks/compass-bash-gate.sh +95 -0
- package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +86 -0
- package/plugins/compass/scripts/hooks/compass-record-write.sh +97 -0
- package/plugins/compass/scripts/hooks/compass-session-start.sh +77 -0
- package/plugins/compass/scripts/lib/compass-config.sh +72 -0
- package/plugins/compass/scripts/lib/compass-evaluator.sh +374 -0
- package/plugins/compass/scripts/lib/compass-events.sh +81 -0
- package/plugins/compass/scripts/lib/compass-gate.sh +465 -0
- package/plugins/compass/scripts/lib/compass-sanitizer.sh +82 -0
- package/plugins/compass/scripts/lib/compass-transcript.sh +135 -0
- package/plugins/governor/.claude-plugin/plugin.json +14 -0
- package/plugins/governor/CHANGELOG.md +22 -0
- package/plugins/governor/config.json +19 -0
- package/plugins/governor/hooks/hooks.json +48 -0
- package/plugins/governor/scripts/hooks/governor-post-tool-use.sh +147 -0
- package/plugins/governor/scripts/hooks/governor-pre-tool-use.sh +199 -0
- package/plugins/governor/scripts/hooks/governor-session-start.sh +109 -0
- package/plugins/governor/scripts/hooks/governor-stop.sh +108 -0
- package/plugins/governor/scripts/lib/governor-config.sh +79 -0
- package/plugins/governor/scripts/lib/governor-estimate.sh +116 -0
- package/plugins/governor/scripts/lib/governor-events.sh +81 -0
- package/plugins/governor/scripts/lib/governor-ledger.sh +172 -0
- package/plugins/scribe/.claude-plugin/plugin.json +12 -0
- package/plugins/scribe/CHANGELOG.md +8 -0
- package/plugins/scribe/config.json +20 -0
- package/plugins/scribe/hooks/hooks.json +37 -0
- package/plugins/scribe/scripts/hooks/scribe-capture.sh +76 -0
- package/plugins/scribe/scripts/hooks/scribe-session-start.sh +58 -0
- package/plugins/scribe/scripts/hooks/scribe-stop.sh +67 -0
- package/plugins/scribe/scripts/lib/scribe-config.sh +72 -0
- package/plugins/scribe/scripts/lib/scribe-distill.sh +239 -0
- package/plugins/scribe/scripts/lib/scribe-events.sh +80 -0
- package/plugins/scribe/scripts/lib/scribe-extract.sh +147 -0
- package/plugins/scribe/scripts/lib/scribe-project-key.sh +89 -0
- package/plugins/scribe/scripts/lib/scribe-ulid.sh +50 -0
- package/release-please-config.json +48 -0
- package/test/bats/governor-config.bats +106 -0
- package/test/bats/governor-estimate.bats +86 -0
- package/test/bats/governor-events.bats +238 -0
- package/test/bats/governor-ledger.bats +220 -0
- package/test/bats/scribe-extract.bats +102 -0
- package/test/bats/scribe-project-key.bats +75 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Project key derivation for Scribe.
|
|
3
|
+
#
|
|
4
|
+
# Mirrors the tribunal project-key scheme so plugins partition storage
|
|
5
|
+
# identically. A project key is a stable 12-char hex identifier that survives:
|
|
6
|
+
# - local rename of the repo directory
|
|
7
|
+
# - cloning the same repo to a different path on the same machine
|
|
8
|
+
# - moving the repo between machines (as long as the git remote is preserved)
|
|
9
|
+
# - worktrees (a worktree shares its parent repo's key)
|
|
10
|
+
#
|
|
11
|
+
# Resolution order:
|
|
12
|
+
# 1. SHA256(`git remote get-url origin`) — preferred, machine-portable
|
|
13
|
+
# 2. SHA256(realpath of `git rev-parse --show-toplevel`) — fallback for repos
|
|
14
|
+
# without an origin remote
|
|
15
|
+
#
|
|
16
|
+
# Returns the first 12 hex chars. Returns empty string if neither path works.
|
|
17
|
+
|
|
18
|
+
_scribe_sha256_first12() {
|
|
19
|
+
local input="$1"
|
|
20
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
21
|
+
printf '%s' "$input" | shasum -a 256 2>/dev/null | cut -c1-12
|
|
22
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
23
|
+
printf '%s' "$input" | sha256sum 2>/dev/null | cut -c1-12
|
|
24
|
+
else
|
|
25
|
+
return 1
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
scribe_project_remote_url() {
|
|
30
|
+
local cwd="${1:-}"
|
|
31
|
+
[[ -z "$cwd" || ! -d "$cwd" ]] && return 0
|
|
32
|
+
git -C "$cwd" remote get-url origin 2>/dev/null || true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Worktree-aware: uses common-dir so worktrees share a key with the main repo.
|
|
36
|
+
scribe_project_repo_root() {
|
|
37
|
+
local cwd="${1:-}"
|
|
38
|
+
[[ -z "$cwd" || ! -d "$cwd" ]] && return 0
|
|
39
|
+
|
|
40
|
+
if ! git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
41
|
+
return 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
local common_dir toplevel
|
|
45
|
+
common_dir=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || return 0
|
|
46
|
+
|
|
47
|
+
if [[ -n "$common_dir" && "$common_dir" != /* ]]; then
|
|
48
|
+
common_dir="$(cd "$cwd" && cd "$common_dir" 2>/dev/null && pwd -P)" || common_dir=""
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [[ -n "$common_dir" && -d "$common_dir" ]]; then
|
|
52
|
+
toplevel="$(cd "$common_dir/.." 2>/dev/null && pwd -P)" || toplevel=""
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if [[ -z "$toplevel" ]]; then
|
|
56
|
+
toplevel=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true)
|
|
57
|
+
[[ -n "$toplevel" ]] && toplevel="$(cd "$toplevel" 2>/dev/null && pwd -P)"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
printf '%s' "$toplevel"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
scribe_project_key() {
|
|
64
|
+
local cwd="${1:-}"
|
|
65
|
+
[[ -z "$cwd" ]] && cwd="$(pwd)"
|
|
66
|
+
|
|
67
|
+
local remote
|
|
68
|
+
remote=$(scribe_project_remote_url "$cwd")
|
|
69
|
+
if [[ -n "$remote" ]]; then
|
|
70
|
+
_scribe_sha256_first12 "remote:$remote"
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
local root
|
|
75
|
+
root=$(scribe_project_repo_root "$cwd")
|
|
76
|
+
if [[ -n "$root" ]]; then
|
|
77
|
+
_scribe_sha256_first12 "root:$root"
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
scribe_project_dir() {
|
|
85
|
+
local project_key="${1:-}"
|
|
86
|
+
[[ -z "$project_key" ]] && return 1
|
|
87
|
+
local onlooker_dir="${ONLOOKER_DIR:-${HOME}/.onlooker}"
|
|
88
|
+
printf '%s' "${onlooker_dir}/scribe/${project_key}"
|
|
89
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Minimal ULID generator for Scribe document and event IDs.
|
|
3
|
+
#
|
|
4
|
+
# Spec: https://github.com/ulid/spec
|
|
5
|
+
# - 48-bit timestamp (ms since epoch) → 10 chars Crockford Base32
|
|
6
|
+
# - 80-bit randomness → 16 chars Crockford Base32
|
|
7
|
+
# - lexicographically sortable, time-ordered
|
|
8
|
+
|
|
9
|
+
_SCRIBE_ULID_ALPHABET="0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
10
|
+
|
|
11
|
+
_scribe_ulid_encode() {
|
|
12
|
+
local n="$1"
|
|
13
|
+
local len="$2"
|
|
14
|
+
local out=""
|
|
15
|
+
local i
|
|
16
|
+
for ((i = 0; i < len; i++)); do
|
|
17
|
+
out="${_SCRIBE_ULID_ALPHABET:$((n % 32)):1}${out}"
|
|
18
|
+
n=$((n / 32))
|
|
19
|
+
done
|
|
20
|
+
printf '%s' "$out"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
scribe_ulid() {
|
|
24
|
+
local now_ms
|
|
25
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
26
|
+
now_ms=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null) \
|
|
27
|
+
|| now_ms=$(($(date +%s) * 1000))
|
|
28
|
+
else
|
|
29
|
+
now_ms=$(date +%s%3N 2>/dev/null) || now_ms=$(($(date +%s) * 1000))
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
local rand_hex rand_hi rand_lo
|
|
33
|
+
rand_hex=$(openssl rand -hex 10 2>/dev/null)
|
|
34
|
+
if [[ -n "$rand_hex" && ${#rand_hex} -eq 20 ]]; then
|
|
35
|
+
rand_hi=$((16#${rand_hex:0:10}))
|
|
36
|
+
rand_lo=$((16#${rand_hex:10:10}))
|
|
37
|
+
else
|
|
38
|
+
rand_hi=$((RANDOM * 32768 + RANDOM))
|
|
39
|
+
rand_lo=$((RANDOM * 32768 + RANDOM))
|
|
40
|
+
rand_hi=$(((rand_hi * 256 + RANDOM % 256) & ((1 << 40) - 1)))
|
|
41
|
+
rand_lo=$(((rand_lo * 256 + RANDOM % 256) & ((1 << 40) - 1)))
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
local ts_part hi_part lo_part
|
|
45
|
+
ts_part=$(_scribe_ulid_encode "$now_ms" 10)
|
|
46
|
+
hi_part=$(_scribe_ulid_encode "$rand_hi" 8)
|
|
47
|
+
lo_part=$(_scribe_ulid_encode "$rand_lo" 8)
|
|
48
|
+
|
|
49
|
+
printf '%s%s%s' "$ts_part" "$hi_part" "$lo_part"
|
|
50
|
+
}
|
|
@@ -78,6 +78,54 @@
|
|
|
78
78
|
"jsonpath": "$.version"
|
|
79
79
|
}
|
|
80
80
|
]
|
|
81
|
+
},
|
|
82
|
+
"plugins/governor": {
|
|
83
|
+
"changelog-path": "CHANGELOG.md",
|
|
84
|
+
"release-type": "simple",
|
|
85
|
+
"bump-minor-pre-major": true,
|
|
86
|
+
"bump-patch-for-minor-pre-major": false,
|
|
87
|
+
"component": "governor",
|
|
88
|
+
"draft": false,
|
|
89
|
+
"prerelease": false,
|
|
90
|
+
"extra-files": [
|
|
91
|
+
{
|
|
92
|
+
"type": "json",
|
|
93
|
+
"path": ".claude-plugin/plugin.json",
|
|
94
|
+
"jsonpath": "$.version"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"plugins/compass": {
|
|
99
|
+
"changelog-path": "CHANGELOG.md",
|
|
100
|
+
"release-type": "simple",
|
|
101
|
+
"bump-minor-pre-major": true,
|
|
102
|
+
"bump-patch-for-minor-pre-major": false,
|
|
103
|
+
"component": "compass",
|
|
104
|
+
"draft": false,
|
|
105
|
+
"prerelease": false,
|
|
106
|
+
"extra-files": [
|
|
107
|
+
{
|
|
108
|
+
"type": "json",
|
|
109
|
+
"path": ".claude-plugin/plugin.json",
|
|
110
|
+
"jsonpath": "$.version"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"plugins/scribe": {
|
|
115
|
+
"changelog-path": "CHANGELOG.md",
|
|
116
|
+
"release-type": "simple",
|
|
117
|
+
"bump-minor-pre-major": true,
|
|
118
|
+
"bump-patch-for-minor-pre-major": false,
|
|
119
|
+
"component": "scribe",
|
|
120
|
+
"draft": false,
|
|
121
|
+
"prerelease": false,
|
|
122
|
+
"extra-files": [
|
|
123
|
+
{
|
|
124
|
+
"type": "json",
|
|
125
|
+
"path": ".claude-plugin/plugin.json",
|
|
126
|
+
"jsonpath": "$.version"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
81
129
|
}
|
|
82
130
|
},
|
|
83
131
|
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/governor"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/governor-config.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "governor is disabled by default" {
|
|
14
|
+
governor_config_load ""
|
|
15
|
+
run governor_config_enabled
|
|
16
|
+
[ "$status" -ne 0 ]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "user-level settings.json can enable governor" {
|
|
20
|
+
mkdir -p "${HOME}/.claude"
|
|
21
|
+
printf '%s\n' '{"governor":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
22
|
+
governor_config_load ""
|
|
23
|
+
run governor_config_enabled
|
|
24
|
+
[ "$status" -eq 0 ]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "repo-level settings.json overrides user-level" {
|
|
28
|
+
mkdir -p "${HOME}/.claude"
|
|
29
|
+
printf '%s\n' '{"governor":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
30
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
31
|
+
mkdir -p "${repo}/.claude"
|
|
32
|
+
printf '%s\n' '{"governor":{"enabled":false}}' > "${repo}/.claude/settings.json"
|
|
33
|
+
governor_config_load "$repo"
|
|
34
|
+
run governor_config_enabled
|
|
35
|
+
[ "$status" -ne 0 ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "default enforcement is soft" {
|
|
39
|
+
governor_config_load ""
|
|
40
|
+
local v
|
|
41
|
+
v=$(governor_config_enforcement)
|
|
42
|
+
[ "$v" = "soft" ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "enforcement can be overridden to hard" {
|
|
46
|
+
mkdir -p "${HOME}/.claude"
|
|
47
|
+
printf '%s\n' '{"governor":{"enforcement":"hard"}}' > "${HOME}/.claude/settings.json"
|
|
48
|
+
governor_config_load ""
|
|
49
|
+
local v
|
|
50
|
+
v=$(governor_config_enforcement)
|
|
51
|
+
[ "$v" = "hard" ]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@test "default tokens budget is 100000" {
|
|
55
|
+
governor_config_load ""
|
|
56
|
+
local v
|
|
57
|
+
v=$(governor_config_get '.governor.session.tokens_default')
|
|
58
|
+
[ "$v" = "100000" ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "default cost budget is 1.0" {
|
|
62
|
+
governor_config_load ""
|
|
63
|
+
local v
|
|
64
|
+
v=$(governor_config_get '.governor.session.cost_usd_default')
|
|
65
|
+
[ "$v" = "1.0" ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "default safety margin is 1.3" {
|
|
69
|
+
governor_config_load ""
|
|
70
|
+
local v
|
|
71
|
+
v=$(governor_config_get '.governor.estimation.safety_margin')
|
|
72
|
+
[ "$v" = "1.3" ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "default hard_stop_margin is 1.5" {
|
|
76
|
+
governor_config_load ""
|
|
77
|
+
local v
|
|
78
|
+
v=$(governor_config_get '.governor.estimation.hard_stop_margin')
|
|
79
|
+
[ "$v" = "1.5" ]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@test "default estimation method is tier_table" {
|
|
83
|
+
governor_config_load ""
|
|
84
|
+
local v
|
|
85
|
+
v=$(governor_config_get '.governor.estimation.method')
|
|
86
|
+
[ "$v" = "tier_table" ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@test "governor_config_get returns empty for missing key" {
|
|
90
|
+
governor_config_load ""
|
|
91
|
+
local v
|
|
92
|
+
v=$(governor_config_get '.governor.no_such_key')
|
|
93
|
+
[ -z "$v" ]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@test "empty repo_root does not load /.claude/settings.json" {
|
|
97
|
+
# Place a settings.json at the absolute root path that an empty repo_root would produce.
|
|
98
|
+
# On a real machine this won't exist, but in CI it might; the guard should skip it.
|
|
99
|
+
# We verify that a file at / does not influence config by confirming the default holds.
|
|
100
|
+
governor_config_load ""
|
|
101
|
+
run governor_config_enabled
|
|
102
|
+
# Default is disabled — if /.claude/settings.json were loaded with {enabled:true}
|
|
103
|
+
# this would fail. We can't plant a file at / in tests, so we assert the default
|
|
104
|
+
# is intact (regression guard rather than direct injection test).
|
|
105
|
+
[ "$status" -ne 0 ]
|
|
106
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/governor"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/governor-config.sh"
|
|
12
|
+
# shellcheck disable=SC1091
|
|
13
|
+
source "${PLUGIN_ROOT}/scripts/lib/governor-estimate.sh"
|
|
14
|
+
|
|
15
|
+
governor_config_load ""
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@test "governor_estimate_method returns tier_table" {
|
|
19
|
+
local m
|
|
20
|
+
m=$(governor_estimate_method)
|
|
21
|
+
[ "$m" = "tier_table" ]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@test "empty input returns a nonzero estimate" {
|
|
25
|
+
local t
|
|
26
|
+
t=$(governor_estimate_tokens "")
|
|
27
|
+
[ "$t" -gt 0 ]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@test "prose input produces a positive estimate" {
|
|
31
|
+
local input="This is a plain English sentence with no special characters at all."
|
|
32
|
+
local t
|
|
33
|
+
t=$(governor_estimate_tokens "$input" 1.0)
|
|
34
|
+
[ "$t" -gt 0 ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "JSON input produces higher token density estimate than prose" {
|
|
38
|
+
local prose="This is a longer plain English paragraph used as baseline comparison text."
|
|
39
|
+
local json='{"key":"value","nested":{"array":[1,2,3,4,5],"flag":true},"extra":"padding"}'
|
|
40
|
+
|
|
41
|
+
local chars_prose=${#prose}
|
|
42
|
+
local chars_json=${#json}
|
|
43
|
+
local toks_prose
|
|
44
|
+
local toks_json
|
|
45
|
+
toks_prose=$(governor_estimate_tokens "$prose" 1.0)
|
|
46
|
+
toks_json=$(governor_estimate_tokens "$json" 1.0)
|
|
47
|
+
|
|
48
|
+
# JSON uses 3 chars/tok vs 4 for prose, so per char JSON should yield more tokens.
|
|
49
|
+
local ratio_prose=$(( toks_prose * 100 / chars_prose ))
|
|
50
|
+
local ratio_json=$(( toks_json * 100 / chars_json ))
|
|
51
|
+
[ "$ratio_json" -ge "$ratio_prose" ]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@test "safety margin multiplies the base estimate" {
|
|
55
|
+
local input="hello world this is a test sentence"
|
|
56
|
+
local base
|
|
57
|
+
local with_margin
|
|
58
|
+
base=$(governor_estimate_tokens "$input" 1.0)
|
|
59
|
+
with_margin=$(governor_estimate_tokens "$input" 1.3)
|
|
60
|
+
|
|
61
|
+
# with_margin should be >= base (margin >= 1.0)
|
|
62
|
+
[ "$with_margin" -ge "$base" ]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test "estimate scales with input length" {
|
|
66
|
+
local short="short"
|
|
67
|
+
local long
|
|
68
|
+
long=$(printf 'x%.0s' {1..500})
|
|
69
|
+
local t_short t_long
|
|
70
|
+
t_short=$(governor_estimate_tokens "$short" 1.0)
|
|
71
|
+
t_long=$(governor_estimate_tokens "$long" 1.0)
|
|
72
|
+
[ "$t_long" -gt "$t_short" ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "governor_estimate_cost returns a positive float for nonzero tokens" {
|
|
76
|
+
local cost
|
|
77
|
+
cost=$(governor_estimate_cost 10000)
|
|
78
|
+
# Should be > 0
|
|
79
|
+
awk "BEGIN { exit ($cost > 0) ? 0 : 1 }"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@test "governor_estimate_cost returns 0-ish for 0 tokens" {
|
|
83
|
+
local cost
|
|
84
|
+
cost=$(governor_estimate_cost 0)
|
|
85
|
+
[ "$cost" = "0.000000" ] || [ "$cost" = "0" ] || [ "$cost" = "0.0" ]
|
|
86
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Validates that governor.* events pass @onlooker-community/schema validation.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/governor"
|
|
10
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
11
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
12
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
13
|
+
|
|
14
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
15
|
+
|
|
16
|
+
# shellcheck disable=SC1091
|
|
17
|
+
source "${PLUGIN_ROOT}/scripts/lib/governor-config.sh"
|
|
18
|
+
# shellcheck disable=SC1091
|
|
19
|
+
source "${PLUGIN_ROOT}/scripts/lib/governor-events.sh"
|
|
20
|
+
|
|
21
|
+
export CLAUDE_SESSION_ID="bats-gov-session-$$"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_validate_latest_event() {
|
|
25
|
+
local last
|
|
26
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
27
|
+
[ -n "$last" ] || return 1
|
|
28
|
+
printf '%s' "$last" | ONLOOKER_DIR="$ONLOOKER_DIR" \
|
|
29
|
+
node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SID="bats-session-000"
|
|
33
|
+
AID="bats-agent-000"
|
|
34
|
+
|
|
35
|
+
@test "governor.gate.checked allow validates" {
|
|
36
|
+
local p
|
|
37
|
+
p=$(jq -n \
|
|
38
|
+
--arg sid "$SID" --arg aid "$AID" \
|
|
39
|
+
'{
|
|
40
|
+
session_id: $sid,
|
|
41
|
+
agent_id: $aid,
|
|
42
|
+
agent_type: "Task",
|
|
43
|
+
decision: "allow",
|
|
44
|
+
estimated_tokens: 5000,
|
|
45
|
+
tokens_available: 95000,
|
|
46
|
+
estimation_method: "tier_table",
|
|
47
|
+
safety_margin: 1.3
|
|
48
|
+
}')
|
|
49
|
+
governor_emit_event "governor.gate.checked" "$p"
|
|
50
|
+
run _validate_latest_event
|
|
51
|
+
[ "$status" -eq 0 ]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@test "governor.gate.checked block with reason validates" {
|
|
55
|
+
local p
|
|
56
|
+
p=$(jq -n \
|
|
57
|
+
--arg sid "$SID" --arg aid "$AID" \
|
|
58
|
+
'{
|
|
59
|
+
session_id: $sid,
|
|
60
|
+
agent_id: $aid,
|
|
61
|
+
agent_type: "Task",
|
|
62
|
+
decision: "block",
|
|
63
|
+
reason: "budget_exceeded",
|
|
64
|
+
estimated_tokens: 110000,
|
|
65
|
+
tokens_available: 5000,
|
|
66
|
+
estimation_method: "tier_table",
|
|
67
|
+
safety_margin: 1.3
|
|
68
|
+
}')
|
|
69
|
+
governor_emit_event "governor.gate.checked" "$p"
|
|
70
|
+
run _validate_latest_event
|
|
71
|
+
[ "$status" -eq 0 ]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@test "governor.call.recorded validates" {
|
|
75
|
+
local p
|
|
76
|
+
p=$(jq -n \
|
|
77
|
+
--arg sid "$SID" --arg aid "$AID" \
|
|
78
|
+
'{
|
|
79
|
+
session_id: $sid,
|
|
80
|
+
agent_id: $aid,
|
|
81
|
+
agent_type: "Task",
|
|
82
|
+
estimated_tokens: 4200,
|
|
83
|
+
cost_usd_estimated: 0.038,
|
|
84
|
+
duration_ms: 3500
|
|
85
|
+
}')
|
|
86
|
+
governor_emit_event "governor.call.recorded" "$p"
|
|
87
|
+
run _validate_latest_event
|
|
88
|
+
[ "$status" -eq 0 ]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@test "governor.call.recorded with actuals validates" {
|
|
92
|
+
local p
|
|
93
|
+
p=$(jq -n \
|
|
94
|
+
--arg sid "$SID" --arg aid "$AID" \
|
|
95
|
+
'{
|
|
96
|
+
session_id: $sid,
|
|
97
|
+
agent_id: $aid,
|
|
98
|
+
agent_type: "Task",
|
|
99
|
+
estimated_tokens: 4200,
|
|
100
|
+
actual_tokens: 3900,
|
|
101
|
+
estimation_error_pct: 7.69,
|
|
102
|
+
cost_usd_estimated: 0.038,
|
|
103
|
+
cost_usd_actual: 0.035,
|
|
104
|
+
duration_ms: 3500,
|
|
105
|
+
tokens_returned_to_pool: 0
|
|
106
|
+
}')
|
|
107
|
+
governor_emit_event "governor.call.recorded" "$p"
|
|
108
|
+
run _validate_latest_event
|
|
109
|
+
[ "$status" -eq 0 ]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@test "governor.ledger.write_failed validates" {
|
|
113
|
+
local p
|
|
114
|
+
p=$(jq -n \
|
|
115
|
+
--arg sid "$SID" --arg aid "$AID" \
|
|
116
|
+
'{
|
|
117
|
+
session_id: $sid,
|
|
118
|
+
agent_id: $aid,
|
|
119
|
+
error: "write failed after 3 attempts",
|
|
120
|
+
retry_count: 3,
|
|
121
|
+
ledger_poisoned: true,
|
|
122
|
+
unrecorded_tokens: 4200
|
|
123
|
+
}')
|
|
124
|
+
governor_emit_event "governor.ledger.write_failed" "$p"
|
|
125
|
+
run _validate_latest_event
|
|
126
|
+
[ "$status" -eq 0 ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@test "governor.budget.warning validates" {
|
|
130
|
+
local p
|
|
131
|
+
p=$(jq -n \
|
|
132
|
+
--arg sid "$SID" \
|
|
133
|
+
'{
|
|
134
|
+
budget_usd: 1.0,
|
|
135
|
+
spent_usd: 0.72,
|
|
136
|
+
threshold_pct: 70,
|
|
137
|
+
remaining_usd: 0.28,
|
|
138
|
+
session_id: $sid,
|
|
139
|
+
dimension: "cost_usd"
|
|
140
|
+
}')
|
|
141
|
+
governor_emit_event "governor.budget.warning" "$p"
|
|
142
|
+
run _validate_latest_event
|
|
143
|
+
[ "$status" -eq 0 ]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@test "governor.budget.exceeded validates" {
|
|
147
|
+
local p
|
|
148
|
+
p=$(jq -n \
|
|
149
|
+
--arg sid "$SID" --arg aid "$AID" \
|
|
150
|
+
'{
|
|
151
|
+
budget_usd: 1.0,
|
|
152
|
+
spent_usd: 1.05,
|
|
153
|
+
blocked_operation: "Task spawn",
|
|
154
|
+
session_id: $sid,
|
|
155
|
+
agent_id: $aid,
|
|
156
|
+
dimension: "tokens",
|
|
157
|
+
estimated_call_cost: 0.08,
|
|
158
|
+
ceiling_type: "session"
|
|
159
|
+
}')
|
|
160
|
+
governor_emit_event "governor.budget.exceeded" "$p"
|
|
161
|
+
run _validate_latest_event
|
|
162
|
+
[ "$status" -eq 0 ]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@test "governor.session.complete validates" {
|
|
166
|
+
local p
|
|
167
|
+
p=$(jq -n \
|
|
168
|
+
--arg sid "$SID" \
|
|
169
|
+
'{
|
|
170
|
+
total_cost_usd: 0.42,
|
|
171
|
+
budget_usd: 1.0,
|
|
172
|
+
under_budget: true,
|
|
173
|
+
session_id: $sid,
|
|
174
|
+
total_tokens: 46200,
|
|
175
|
+
total_api_calls: 11,
|
|
176
|
+
duration_ms: 0,
|
|
177
|
+
calls_blocked: 0,
|
|
178
|
+
calls_warned: 2,
|
|
179
|
+
ledger_poisoned: false
|
|
180
|
+
}')
|
|
181
|
+
governor_emit_event "governor.session.complete" "$p"
|
|
182
|
+
run _validate_latest_event
|
|
183
|
+
[ "$status" -eq 0 ]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@test "governor.lock.stale_cleared validates" {
|
|
187
|
+
local p
|
|
188
|
+
p=$(jq -n \
|
|
189
|
+
'{
|
|
190
|
+
lock_path: "/tmp/test.lock.d",
|
|
191
|
+
lock_age_seconds: 120.5,
|
|
192
|
+
pid_verified_dead: false
|
|
193
|
+
}')
|
|
194
|
+
governor_emit_event "governor.lock.stale_cleared" "$p"
|
|
195
|
+
run _validate_latest_event
|
|
196
|
+
[ "$status" -eq 0 ]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@test "governor.child.allocated validates" {
|
|
200
|
+
local p
|
|
201
|
+
p=$(jq -n \
|
|
202
|
+
--arg sid "$SID" \
|
|
203
|
+
'{
|
|
204
|
+
session_id: $sid,
|
|
205
|
+
parent_agent_id: "parent-001",
|
|
206
|
+
child_agent_id: "child-001",
|
|
207
|
+
child_agent_type: "tribunal-actor",
|
|
208
|
+
tokens_allocated: 20000,
|
|
209
|
+
cost_usd_allocated: 0.18,
|
|
210
|
+
tokens_remaining_after_allocation: 80000,
|
|
211
|
+
conservation_check_passed: true
|
|
212
|
+
}')
|
|
213
|
+
governor_emit_event "governor.child.allocated" "$p"
|
|
214
|
+
run _validate_latest_event
|
|
215
|
+
[ "$status" -eq 0 ]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@test "governor.child.returned validates" {
|
|
219
|
+
local p
|
|
220
|
+
p=$(jq -n \
|
|
221
|
+
--arg sid "$SID" \
|
|
222
|
+
'{
|
|
223
|
+
session_id: $sid,
|
|
224
|
+
parent_agent_id: "parent-001",
|
|
225
|
+
child_agent_id: "child-001",
|
|
226
|
+
tokens_allocated: 20000,
|
|
227
|
+
tokens_consumed: 14200,
|
|
228
|
+
tokens_returned: 5800
|
|
229
|
+
}')
|
|
230
|
+
governor_emit_event "governor.child.returned" "$p"
|
|
231
|
+
run _validate_latest_event
|
|
232
|
+
[ "$status" -eq 0 ]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@test "governor_emit_event returns nonzero for unknown event type" {
|
|
236
|
+
run governor_emit_event "governor.no_such_event" '{"session_id":"x"}'
|
|
237
|
+
[ "$status" -ne 0 ]
|
|
238
|
+
}
|