@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.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +39 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.release-please-manifest.json +5 -2
  4. package/CHANGELOG.md +15 -0
  5. package/CLAUDE.md +88 -0
  6. package/package.json +3 -3
  7. package/plugins/compass/.claude-plugin/plugin.json +14 -0
  8. package/plugins/compass/CHANGELOG.md +8 -0
  9. package/plugins/compass/config.json +71 -0
  10. package/plugins/compass/docs/adr/001-evaluate-prompts-in-context.md +82 -0
  11. package/plugins/compass/docs/design.md +421 -0
  12. package/plugins/compass/hooks/hooks.json +82 -0
  13. package/plugins/compass/scripts/hooks/compass-bash-gate.sh +95 -0
  14. package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +86 -0
  15. package/plugins/compass/scripts/hooks/compass-record-write.sh +97 -0
  16. package/plugins/compass/scripts/hooks/compass-session-start.sh +77 -0
  17. package/plugins/compass/scripts/lib/compass-config.sh +72 -0
  18. package/plugins/compass/scripts/lib/compass-evaluator.sh +374 -0
  19. package/plugins/compass/scripts/lib/compass-events.sh +81 -0
  20. package/plugins/compass/scripts/lib/compass-gate.sh +465 -0
  21. package/plugins/compass/scripts/lib/compass-sanitizer.sh +82 -0
  22. package/plugins/compass/scripts/lib/compass-transcript.sh +135 -0
  23. package/plugins/governor/.claude-plugin/plugin.json +14 -0
  24. package/plugins/governor/CHANGELOG.md +22 -0
  25. package/plugins/governor/config.json +19 -0
  26. package/plugins/governor/hooks/hooks.json +48 -0
  27. package/plugins/governor/scripts/hooks/governor-post-tool-use.sh +147 -0
  28. package/plugins/governor/scripts/hooks/governor-pre-tool-use.sh +199 -0
  29. package/plugins/governor/scripts/hooks/governor-session-start.sh +109 -0
  30. package/plugins/governor/scripts/hooks/governor-stop.sh +108 -0
  31. package/plugins/governor/scripts/lib/governor-config.sh +79 -0
  32. package/plugins/governor/scripts/lib/governor-estimate.sh +116 -0
  33. package/plugins/governor/scripts/lib/governor-events.sh +81 -0
  34. package/plugins/governor/scripts/lib/governor-ledger.sh +172 -0
  35. package/plugins/scribe/.claude-plugin/plugin.json +12 -0
  36. package/plugins/scribe/CHANGELOG.md +8 -0
  37. package/plugins/scribe/config.json +20 -0
  38. package/plugins/scribe/hooks/hooks.json +37 -0
  39. package/plugins/scribe/scripts/hooks/scribe-capture.sh +76 -0
  40. package/plugins/scribe/scripts/hooks/scribe-session-start.sh +58 -0
  41. package/plugins/scribe/scripts/hooks/scribe-stop.sh +67 -0
  42. package/plugins/scribe/scripts/lib/scribe-config.sh +72 -0
  43. package/plugins/scribe/scripts/lib/scribe-distill.sh +239 -0
  44. package/plugins/scribe/scripts/lib/scribe-events.sh +80 -0
  45. package/plugins/scribe/scripts/lib/scribe-extract.sh +147 -0
  46. package/plugins/scribe/scripts/lib/scribe-project-key.sh +89 -0
  47. package/plugins/scribe/scripts/lib/scribe-ulid.sh +50 -0
  48. package/release-please-config.json +48 -0
  49. package/test/bats/governor-config.bats +106 -0
  50. package/test/bats/governor-estimate.bats +86 -0
  51. package/test/bats/governor-events.bats +238 -0
  52. package/test/bats/governor-ledger.bats +220 -0
  53. package/test/bats/scribe-extract.bats +102 -0
  54. 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
+ }