@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,14 @@
1
+ {
2
+ "name": "governor",
3
+ "version": "0.2.0",
4
+ "description": "Resource governance and budget enforcement for the Onlooker ecosystem. Tracks per-session token and cost spend, gates Task spawns before they exceed a configurable budget ceiling, and emits governor.* events for audit. Named for the steam-engine governor — a device that regulates output. Builds on the Onlooker ecosystem plugin.",
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,22 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/onlooker-community/ecosystem/compare/governor-v0.1.0...governor-v0.2.0) (2026-05-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * **governor:** resource governance and budget enforcement plugin :rocket: ([#43](https://github.com/onlooker-community/ecosystem/issues/43)) ([04e6d70](https://github.com/onlooker-community/ecosystem/commit/04e6d7051f27db752bb121d389d65b4d8ade04ad))
9
+
10
+ ## [0.1.0] - 2026-05-25
11
+
12
+ ### Added
13
+
14
+ - Initial plugin scaffold: `config.json`, `plugin.json`, `hooks.json`
15
+ - `governor-config.sh` — three-layer config resolution (plugin defaults → user settings → repo settings)
16
+ - `governor-events.sh` — canonical `governor.*` event emission via ecosystem `onlooker-event.mjs`
17
+ - `governor-ledger.sh` — JSONL ledger read/write with `portable-lock.sh` atomic guard
18
+ - `governor-estimate.sh` — tier-table token estimation with configurable safety margin
19
+ - `governor-session-start.sh` — SessionStart hook: setup storage, load budget contract, sweep stale locks, check global policy hash
20
+ - `governor-pre-tool-use.sh` — PreToolUse hook on Task: pre-call gate via check-and-reserve with `portable-lock.sh`
21
+ - `governor-post-tool-use.sh` — PostToolUse hook on Task: record call duration and estimated tokens to JSONL ledger
22
+ - `governor-stop.sh` — Stop hook: emit `governor.session.complete` with cumulative spend summary
@@ -0,0 +1,19 @@
1
+ {
2
+ "plugin_name": "governor",
3
+ "storage_path": "~/.onlooker",
4
+ "governor": {
5
+ "enabled": false,
6
+ "enforcement": "soft",
7
+ "global_policy_path": "~/.onlooker/governance/global-policy.yaml",
8
+ "session": {
9
+ "tokens_default": 100000,
10
+ "cost_usd_default": 1.0,
11
+ "reserve_pct": 10
12
+ },
13
+ "estimation": {
14
+ "safety_margin": 1.3,
15
+ "hard_stop_margin": 1.5,
16
+ "method": "tier_table"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/governor-session-start.sh"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PreToolUse": [
15
+ {
16
+ "matcher": "Task",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/governor-pre-tool-use.sh"
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "PostToolUse": [
26
+ {
27
+ "matcher": "Task",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/governor-post-tool-use.sh"
32
+ }
33
+ ]
34
+ }
35
+ ],
36
+ "Stop": [
37
+ {
38
+ "matcher": "*",
39
+ "hooks": [
40
+ {
41
+ "type": "command",
42
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/governor-stop.sh"
43
+ }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ }
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # Governor PostToolUse hook (matcher: Task).
3
+ #
4
+ # Records each completed Task call in the JSONL ledger. Validates whether
5
+ # the PostToolUse payload includes actual usage counts (Q1 from issue #40).
6
+ #
7
+ # Hook contract:
8
+ # - Always exits 0. Recording failure must never block the session.
9
+ # - Skips silently when governor.enabled is false.
10
+
11
+ set -uo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
15
+
16
+ _ECOSYSTEM_ROOT="${ONLOOKER_ECOSYSTEM_ROOT:-}"
17
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
18
+ _candidate="$(cd "${PLUGIN_ROOT}/../.." 2>/dev/null && pwd)"
19
+ if [[ -f "${_candidate}/scripts/lib/validate-path.sh" ]]; then
20
+ _ECOSYSTEM_ROOT="$_candidate"
21
+ fi
22
+ fi
23
+
24
+ if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
25
+ # shellcheck disable=SC1091
26
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
27
+ # shellcheck disable=SC1091
28
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/portable-lock.sh"
29
+ fi
30
+
31
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
32
+
33
+ # shellcheck source=../lib/governor-config.sh
34
+ source "${PLUGIN_ROOT}/scripts/lib/governor-config.sh"
35
+ # shellcheck source=../lib/governor-events.sh
36
+ source "${PLUGIN_ROOT}/scripts/lib/governor-events.sh"
37
+ # shellcheck source=../lib/governor-estimate.sh
38
+ source "${PLUGIN_ROOT}/scripts/lib/governor-estimate.sh"
39
+ # shellcheck source=../lib/governor-ledger.sh
40
+ source "${PLUGIN_ROOT}/scripts/lib/governor-ledger.sh"
41
+
42
+ _done() { exit 0; }
43
+
44
+ INPUT=$(cat)
45
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
46
+ [[ -z "$SESSION_ID" ]] && SESSION_ID="${CLAUDE_SESSION_ID:-unknown}"
47
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
48
+
49
+ governor_config_load "$CWD"
50
+
51
+ if ! governor_config_enabled; then
52
+ _done
53
+ fi
54
+
55
+ # -----------------------------------------------------------------------
56
+ # Extract hook fields.
57
+ # -----------------------------------------------------------------------
58
+ TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null) || TOOL_NAME=""
59
+ TOOL_INPUT=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) || TOOL_INPUT="{}"
60
+ TOOL_RESPONSE=$(printf '%s' "$INPUT" | jq -c '.tool_response // {}' 2>/dev/null) || TOOL_RESPONSE="{}"
61
+ DURATION_MS=$(printf '%s' "$INPUT" | jq -r '.duration_ms // 0' 2>/dev/null) || DURATION_MS=0
62
+
63
+ # Check if actual usage counts are present (Q1 validation).
64
+ ACTUAL_INPUT_TOKENS=$(printf '%s' "$TOOL_RESPONSE" \
65
+ | jq -r '.usage.input_tokens // .usage.input_tokens_total // empty' 2>/dev/null) \
66
+ || ACTUAL_INPUT_TOKENS=""
67
+ ACTUAL_OUTPUT_TOKENS=$(printf '%s' "$TOOL_RESPONSE" \
68
+ | jq -r '.usage.output_tokens // .usage.output_tokens_total // empty' 2>/dev/null) \
69
+ || ACTUAL_OUTPUT_TOKENS=""
70
+
71
+ # Estimate tokens from the input we sent.
72
+ ESTIMATED_TOKENS=$(governor_estimate_tokens "$TOOL_INPUT")
73
+ ESTIMATED_COST=$(governor_estimate_cost "$ESTIMATED_TOKENS")
74
+ ESTIMATION_METHOD=$(governor_estimate_method)
75
+
76
+ # Build the completion ledger record.
77
+ # estimated_tokens is negated to cancel the reservation written by PreToolUse.
78
+ # actual_tokens (when present) complete the two-phase accounting so the running
79
+ # total converges to real spend: N_est + (-N_est) + N_act = N_act.
80
+ AGENT_TYPE="${TOOL_NAME:-Task}"
81
+ TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null) || TS="1970-01-01T00:00:00Z"
82
+ NEG_ESTIMATED=$(( -ESTIMATED_TOKENS ))
83
+
84
+ RECORD=$(jq -n \
85
+ --arg ts "$TS" \
86
+ --arg sid "$SESSION_ID" \
87
+ --arg aid "${CLAUDE_SESSION_ID:-unknown}" \
88
+ --arg at "$AGENT_TYPE" \
89
+ --argjson est "$NEG_ESTIMATED" \
90
+ --argjson cost "$ESTIMATED_COST" \
91
+ --argjson dur "$DURATION_MS" \
92
+ '{
93
+ ts: $ts,
94
+ session_id: $sid,
95
+ agent_id: $aid,
96
+ agent_type: $at,
97
+ estimated_tokens: $est,
98
+ cost_usd_estimated: $cost,
99
+ duration_ms: $dur
100
+ }' 2>/dev/null) || RECORD="{}"
101
+
102
+ # Compute actual total once; used for both the ledger record and the event payload.
103
+ ACTUAL_TOTAL=""
104
+ if [[ -n "$ACTUAL_INPUT_TOKENS" && -n "$ACTUAL_OUTPUT_TOKENS" ]]; then
105
+ ACTUAL_TOTAL=$(( ACTUAL_INPUT_TOKENS + ACTUAL_OUTPUT_TOKENS ))
106
+ RECORD=$(printf '%s' "$RECORD" | jq \
107
+ --argjson actual "$ACTUAL_TOTAL" \
108
+ '. + {actual_tokens: $actual}' 2>/dev/null) || true
109
+ fi
110
+
111
+ governor_ledger_append "$SESSION_ID" "$RECORD" || true
112
+
113
+ # Build the governor.call.recorded payload.
114
+ CALL_PAYLOAD=$(jq -n \
115
+ --arg sid "$SESSION_ID" \
116
+ --arg aid "${CLAUDE_SESSION_ID:-unknown}" \
117
+ --arg at "$AGENT_TYPE" \
118
+ --argjson est "$ESTIMATED_TOKENS" \
119
+ --argjson cost "$ESTIMATED_COST" \
120
+ --argjson dur "$DURATION_MS" \
121
+ '{
122
+ session_id: $sid,
123
+ agent_id: $aid,
124
+ agent_type: $at,
125
+ estimated_tokens: $est,
126
+ cost_usd_estimated: $cost,
127
+ duration_ms: $dur
128
+ }' 2>/dev/null) || CALL_PAYLOAD="{}"
129
+
130
+ if [[ -n "$ACTUAL_TOTAL" ]]; then
131
+ ESTIMATION_ERROR=""
132
+ if (( ACTUAL_TOTAL > 0 )); then
133
+ ESTIMATION_ERROR=$(awk \
134
+ "BEGIN { printf \"%.2f\", (($ESTIMATED_TOKENS - $ACTUAL_TOTAL) / $ACTUAL_TOTAL) * 100 }" \
135
+ 2>/dev/null) || ESTIMATION_ERROR=""
136
+ fi
137
+ CALL_PAYLOAD=$(printf '%s' "$CALL_PAYLOAD" | jq \
138
+ --argjson actual "$ACTUAL_TOTAL" \
139
+ --arg err "${ESTIMATION_ERROR:-}" \
140
+ '. + {actual_tokens: $actual, tokens_returned_to_pool: 0}
141
+ + (if $err != "" then {estimation_error_pct: ($err | tonumber)} else {} end)' \
142
+ 2>/dev/null) || true
143
+ fi
144
+
145
+ governor_emit_event "governor.call.recorded" "$CALL_PAYLOAD" || true
146
+
147
+ _done
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env bash
2
+ # Governor PreToolUse hook (matcher: Task).
3
+ #
4
+ # Gates Task spawns before they exceed the session budget. Uses
5
+ # portable-lock.sh for an atomic check-and-reserve so concurrent spawns
6
+ # cannot both pass a budget check simultaneously.
7
+ #
8
+ # Decision logic:
9
+ # - Estimate tokens for the spawn.
10
+ # - Read current consumed tokens from the JSONL ledger.
11
+ # - Allow if (consumed + estimated) <= budget_tokens.
12
+ # - Emit governor.gate.checked with decision and reason.
13
+ # - In "soft" enforcement: always allow, only emit the event.
14
+ # - In "hard" enforcement: block by returning {"decision": "block"} on
15
+ # stdout with exit 0 (Claude Code PreToolUse block protocol).
16
+ #
17
+ # Hook contract:
18
+ # - Exit 0 always.
19
+ # - To block: write {"decision": "block", "reason": "..."} to stdout.
20
+ # - To allow: write nothing (or {"decision": "allow"}) to stdout.
21
+ # - Skips silently when governor.enabled is false.
22
+
23
+ set -uo pipefail
24
+
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
27
+
28
+ _ECOSYSTEM_ROOT="${ONLOOKER_ECOSYSTEM_ROOT:-}"
29
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
30
+ _candidate="$(cd "${PLUGIN_ROOT}/../.." 2>/dev/null && pwd)"
31
+ if [[ -f "${_candidate}/scripts/lib/validate-path.sh" ]]; then
32
+ _ECOSYSTEM_ROOT="$_candidate"
33
+ fi
34
+ fi
35
+
36
+ if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
37
+ # shellcheck disable=SC1091
38
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
39
+ # shellcheck disable=SC1091
40
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/portable-lock.sh"
41
+ fi
42
+
43
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
44
+
45
+ # shellcheck source=../lib/governor-config.sh
46
+ source "${PLUGIN_ROOT}/scripts/lib/governor-config.sh"
47
+ # shellcheck source=../lib/governor-events.sh
48
+ source "${PLUGIN_ROOT}/scripts/lib/governor-events.sh"
49
+ # shellcheck source=../lib/governor-estimate.sh
50
+ source "${PLUGIN_ROOT}/scripts/lib/governor-estimate.sh"
51
+ # shellcheck source=../lib/governor-ledger.sh
52
+ source "${PLUGIN_ROOT}/scripts/lib/governor-ledger.sh"
53
+
54
+ _allow() { exit 0; }
55
+
56
+ _block() {
57
+ local reason="${1:-budget_exceeded}"
58
+ printf '{"decision":"block","reason":"%s"}\n' "$reason"
59
+ exit 0
60
+ }
61
+
62
+ INPUT=$(cat)
63
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
64
+ [[ -z "$SESSION_ID" ]] && SESSION_ID="${CLAUDE_SESSION_ID:-unknown}"
65
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
66
+
67
+ governor_config_load "$CWD"
68
+
69
+ if ! governor_config_enabled; then
70
+ _allow
71
+ fi
72
+
73
+ # -----------------------------------------------------------------------
74
+ # Read config.
75
+ # -----------------------------------------------------------------------
76
+ ENFORCEMENT=$(governor_config_enforcement)
77
+ TOKENS_BUDGET=$(governor_config_get '.governor.session.tokens_default')
78
+ TOKENS_BUDGET="${TOKENS_BUDGET:-100000}"
79
+ SAFETY_MARGIN=$(governor_config_get '.governor.estimation.safety_margin')
80
+ SAFETY_MARGIN="${SAFETY_MARGIN:-1.3}"
81
+ HARD_STOP_MARGIN=$(governor_config_get '.governor.estimation.hard_stop_margin')
82
+ HARD_STOP_MARGIN="${HARD_STOP_MARGIN:-1.5}"
83
+
84
+ # Respect env-var budget overrides set by orchestrating agents.
85
+ if [[ -n "${ONLOOKER_SESSION_BUDGET_TOKENS:-}" ]]; then
86
+ TOKENS_BUDGET="$ONLOOKER_SESSION_BUDGET_TOKENS"
87
+ fi
88
+
89
+ TOOL_INPUT=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null) || TOOL_INPUT="{}"
90
+ AGENT_TYPE=$(printf '%s' "$INPUT" | jq -r '.tool_name // "Task"' 2>/dev/null) || AGENT_TYPE="Task"
91
+
92
+ # -----------------------------------------------------------------------
93
+ # Estimate tokens for this spawn.
94
+ # -----------------------------------------------------------------------
95
+ ESTIMATED_TOKENS=$(governor_estimate_tokens "$TOOL_INPUT" "$SAFETY_MARGIN")
96
+ ESTIMATED_COST=$(governor_estimate_cost "$ESTIMATED_TOKENS")
97
+ ESTIMATION_METHOD=$(governor_estimate_method)
98
+
99
+ # -----------------------------------------------------------------------
100
+ # Atomic check-and-reserve via the ledger lock.
101
+ # -----------------------------------------------------------------------
102
+ LEDGER_PATH=$(governor_ledger_path "$SESSION_ID")
103
+ GATE_LOCK="${LEDGER_PATH}.gate.lock"
104
+
105
+ DECISION="allow"
106
+ REASON=""
107
+ TOKENS_CONSUMED=0
108
+
109
+ if lock_acquire "$GATE_LOCK" 3; then
110
+ TOKENS_CONSUMED=$(governor_ledger_total_tokens "$SESSION_ID")
111
+ PROJECTED=$(( TOKENS_CONSUMED + ESTIMATED_TOKENS ))
112
+
113
+ # Hard stop: unconditionally block when projected exceeds budget * hard_stop_margin.
114
+ HARD_STOP_THRESHOLD=$(awk "BEGIN { printf \"%d\", int($TOKENS_BUDGET * $HARD_STOP_MARGIN) }" 2>/dev/null) \
115
+ || HARD_STOP_THRESHOLD=$(( TOKENS_BUDGET * 2 ))
116
+
117
+ if (( PROJECTED > HARD_STOP_THRESHOLD )); then
118
+ DECISION="block"
119
+ REASON="ceiling_exceeded"
120
+ elif (( PROJECTED > TOKENS_BUDGET )); then
121
+ DECISION="block"
122
+ REASON="budget_exceeded"
123
+ fi
124
+
125
+ # Write reservation inside the gate lock so concurrent spawns see in-flight cost.
126
+ if [[ "$DECISION" == "allow" ]]; then
127
+ TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null) || TS="1970-01-01T00:00:00Z"
128
+ RESERVATION=$(jq -n \
129
+ --arg ts "$TS" \
130
+ --arg sid "$SESSION_ID" \
131
+ --arg aid "${CLAUDE_SESSION_ID:-unknown}" \
132
+ --arg at "$AGENT_TYPE" \
133
+ --argjson est "$ESTIMATED_TOKENS" \
134
+ --argjson cost "$ESTIMATED_COST" \
135
+ '{
136
+ ts: $ts,
137
+ session_id: $sid,
138
+ agent_id: $aid,
139
+ agent_type: $at,
140
+ estimated_tokens: $est,
141
+ cost_usd_estimated: $cost,
142
+ record_type: "reservation"
143
+ }' 2>/dev/null) || RESERVATION="{}"
144
+ governor_ledger_write_direct "$LEDGER_PATH" "$RESERVATION" || true
145
+ fi
146
+
147
+ lock_release "$GATE_LOCK"
148
+ else
149
+ # Could not acquire gate lock — treat as block to be safe in hard mode.
150
+ DECISION="block"
151
+ REASON="lock_timeout"
152
+ fi
153
+
154
+ TOKENS_AVAILABLE=$(( TOKENS_BUDGET - TOKENS_CONSUMED ))
155
+ (( TOKENS_AVAILABLE < 0 )) && TOKENS_AVAILABLE=0
156
+
157
+ # -----------------------------------------------------------------------
158
+ # Emit governor.gate.checked.
159
+ # -----------------------------------------------------------------------
160
+ GATE_PAYLOAD=$(jq -n \
161
+ --arg sid "$SESSION_ID" \
162
+ --arg aid "${CLAUDE_SESSION_ID:-unknown}" \
163
+ --arg at "$AGENT_TYPE" \
164
+ --arg dec "$DECISION" \
165
+ --argjson est "$ESTIMATED_TOKENS" \
166
+ --argjson avail "$TOKENS_AVAILABLE" \
167
+ --arg method "$ESTIMATION_METHOD" \
168
+ --argjson margin "$SAFETY_MARGIN" \
169
+ '{
170
+ session_id: $sid,
171
+ agent_id: $aid,
172
+ agent_type: $at,
173
+ decision: $dec,
174
+ estimated_tokens: $est,
175
+ tokens_available: $avail,
176
+ estimation_method: $method,
177
+ safety_margin: $margin
178
+ }' 2>/dev/null) || GATE_PAYLOAD="{}"
179
+
180
+ if [[ -n "$REASON" ]]; then
181
+ GATE_PAYLOAD=$(printf '%s' "$GATE_PAYLOAD" \
182
+ | jq --arg r "$REASON" '. + {reason: $r}' 2>/dev/null) \
183
+ || true
184
+ fi
185
+
186
+ governor_emit_event "governor.gate.checked" "$GATE_PAYLOAD" || true
187
+
188
+ # -----------------------------------------------------------------------
189
+ # Enforce decision.
190
+ # -----------------------------------------------------------------------
191
+ # ceiling_exceeded always blocks regardless of enforcement mode.
192
+ # budget_exceeded and lock_timeout only block in hard enforcement mode.
193
+ if [[ "$DECISION" == "block" ]]; then
194
+ if [[ "$REASON" == "ceiling_exceeded" || "$ENFORCEMENT" == "hard" ]]; then
195
+ _block "${REASON:-budget_exceeded}"
196
+ fi
197
+ fi
198
+
199
+ _allow
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # Governor SessionStart hook.
3
+ #
4
+ # Fires at every session start. Responsibilities:
5
+ # 1. Skip silently when governor.enabled is false.
6
+ # 2. Create governance storage directories.
7
+ # 3. Sweep stale lock files left by crashed prior sessions.
8
+ # 4. Check global-policy.yaml exists (warn if missing, don't block).
9
+ # 5. Emit governor.lock.stale_cleared for each stale lock removed.
10
+ #
11
+ # Hook contract:
12
+ # - Always exits 0. Never blocks SessionStart.
13
+ # - Errors are written to stderr only; stdout is kept clean.
14
+
15
+ set -uo pipefail
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
19
+
20
+ _ECOSYSTEM_ROOT="${ONLOOKER_ECOSYSTEM_ROOT:-}"
21
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
22
+ _candidate="$(cd "${PLUGIN_ROOT}/../.." 2>/dev/null && pwd)"
23
+ if [[ -f "${_candidate}/scripts/lib/validate-path.sh" ]]; then
24
+ _ECOSYSTEM_ROOT="$_candidate"
25
+ fi
26
+ fi
27
+
28
+ if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
29
+ # shellcheck disable=SC1091
30
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
31
+ # shellcheck disable=SC1091
32
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/portable-lock.sh"
33
+ fi
34
+
35
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
36
+
37
+ # shellcheck source=../lib/governor-config.sh
38
+ source "${PLUGIN_ROOT}/scripts/lib/governor-config.sh"
39
+ # shellcheck source=../lib/governor-events.sh
40
+ source "${PLUGIN_ROOT}/scripts/lib/governor-events.sh"
41
+
42
+ INPUT=$(cat)
43
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
44
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
45
+
46
+ _done() { exit 0; }
47
+
48
+ governor_config_load "$CWD"
49
+
50
+ if ! governor_config_enabled; then
51
+ _done
52
+ fi
53
+
54
+ # -----------------------------------------------------------------------
55
+ # 1. Ensure storage directories exist.
56
+ # -----------------------------------------------------------------------
57
+ ONLOOKER_DIR="${ONLOOKER_DIR:-${HOME}/.onlooker}"
58
+ GOVERNANCE_DIR="${ONLOOKER_DIR}/governance"
59
+ LEDGER_DIR="${GOVERNANCE_DIR}/ledgers"
60
+ mkdir -p "$LEDGER_DIR" 2>/dev/null || true
61
+
62
+ # -----------------------------------------------------------------------
63
+ # 2. Sweep stale lock directories.
64
+ # A lock dir is "stale" if it is older than 60 seconds.
65
+ # We only clean .lock.d directories under the ledgers directory.
66
+ # -----------------------------------------------------------------------
67
+ STALE_AGE=60
68
+
69
+ if [[ -d "$LEDGER_DIR" ]]; then
70
+ while IFS= read -r -d '' lockdir; do
71
+ [[ -d "$lockdir" ]] || continue
72
+
73
+ lock_age=0
74
+ if command -v stat >/dev/null 2>&1; then
75
+ # macOS stat: -f %m; GNU stat: -c %Y
76
+ mtime=$(stat -f '%m' "$lockdir" 2>/dev/null) \
77
+ || mtime=$(stat -c '%Y' "$lockdir" 2>/dev/null) \
78
+ || mtime=0
79
+ now=$(date +%s 2>/dev/null) || now=0
80
+ lock_age=$(( now - mtime ))
81
+ fi
82
+
83
+ if (( lock_age >= STALE_AGE )); then
84
+ rmdir "$lockdir" 2>/dev/null || true
85
+ cleared_payload=$(jq -n \
86
+ --arg lp "$lockdir" \
87
+ --argjson age "$lock_age" \
88
+ '{
89
+ lock_path: $lp,
90
+ lock_age_seconds: $age,
91
+ pid_verified_dead: false
92
+ }' 2>/dev/null) || cleared_payload="{}"
93
+ governor_emit_event "governor.lock.stale_cleared" "$cleared_payload" || true
94
+ fi
95
+ done < <(find "$LEDGER_DIR" -maxdepth 2 -name '*.lock.d' -print0 2>/dev/null)
96
+ fi
97
+
98
+ # -----------------------------------------------------------------------
99
+ # 3. Global policy file check (advisory only).
100
+ # -----------------------------------------------------------------------
101
+ POLICY_PATH=$(governor_config_get '.governor.global_policy_path')
102
+ POLICY_PATH="${POLICY_PATH/#\~/$HOME}"
103
+
104
+ if [[ -n "$POLICY_PATH" && ! -f "$POLICY_PATH" ]]; then
105
+ printf 'governor: global-policy.yaml not found at %s — running without global ceiling\n' \
106
+ "$POLICY_PATH" >&2
107
+ fi
108
+
109
+ _done
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # Governor Stop hook.
3
+ #
4
+ # Fires at session end. Emits governor.session.complete with cumulative
5
+ # spend totals from the JSONL ledger.
6
+ #
7
+ # Hook contract:
8
+ # - Always exits 0. Never blocks Stop.
9
+ # - Skips silently when governor.enabled is false.
10
+ # - Errors from ledger reads are swallowed; emits best-effort totals.
11
+
12
+ set -uo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
16
+
17
+ _ECOSYSTEM_ROOT="${ONLOOKER_ECOSYSTEM_ROOT:-}"
18
+ if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
19
+ _candidate="$(cd "${PLUGIN_ROOT}/../.." 2>/dev/null && pwd)"
20
+ if [[ -f "${_candidate}/scripts/lib/validate-path.sh" ]]; then
21
+ _ECOSYSTEM_ROOT="$_candidate"
22
+ fi
23
+ fi
24
+
25
+ if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
26
+ # shellcheck disable=SC1091
27
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
28
+ # shellcheck disable=SC1091
29
+ CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/portable-lock.sh"
30
+ fi
31
+
32
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
33
+
34
+ # shellcheck source=../lib/governor-config.sh
35
+ source "${PLUGIN_ROOT}/scripts/lib/governor-config.sh"
36
+ # shellcheck source=../lib/governor-events.sh
37
+ source "${PLUGIN_ROOT}/scripts/lib/governor-events.sh"
38
+ # shellcheck source=../lib/governor-ledger.sh
39
+ source "${PLUGIN_ROOT}/scripts/lib/governor-ledger.sh"
40
+
41
+ _done() { exit 0; }
42
+
43
+ INPUT=$(cat)
44
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
45
+ [[ -z "$SESSION_ID" ]] && SESSION_ID="${CLAUDE_SESSION_ID:-unknown}"
46
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
47
+
48
+ governor_config_load "$CWD"
49
+
50
+ if ! governor_config_enabled; then
51
+ _done
52
+ fi
53
+
54
+ # -----------------------------------------------------------------------
55
+ # Read session totals from the ledger.
56
+ # -----------------------------------------------------------------------
57
+ TOTAL_TOKENS=$(governor_ledger_total_tokens "$SESSION_ID")
58
+ TOTAL_COST=$(governor_ledger_total_cost "$SESSION_ID")
59
+ TOTAL_CALLS=$(governor_ledger_call_count "$SESSION_ID")
60
+ LEDGER_POISONED=$(governor_ledger_is_poisoned "$SESSION_ID" && printf 'true' || printf 'false')
61
+
62
+ TOKENS_BUDGET=$(governor_config_get '.governor.session.tokens_default')
63
+ TOKENS_BUDGET="${TOKENS_BUDGET:-100000}"
64
+ COST_BUDGET=$(governor_config_get '.governor.session.cost_usd_default')
65
+ COST_BUDGET="${COST_BUDGET:-1.0}"
66
+
67
+ if [[ -n "${ONLOOKER_SESSION_BUDGET_TOKENS:-}" ]]; then
68
+ TOKENS_BUDGET="$ONLOOKER_SESSION_BUDGET_TOKENS"
69
+ fi
70
+
71
+ UNDER_BUDGET="true"
72
+ TOTAL_TOKENS_INT=$(printf '%s' "${TOTAL_TOKENS:-0}" | grep -oE '^[0-9]+' || printf '0')
73
+ TOKENS_BUDGET_INT=$(printf '%s' "${TOKENS_BUDGET:-0}" | grep -oE '^[0-9]+' || printf '0')
74
+ (( TOTAL_TOKENS_INT > TOKENS_BUDGET_INT )) && UNDER_BUDGET="false"
75
+
76
+ # Also check the cost dimension (float comparison via awk).
77
+ if [[ "$UNDER_BUDGET" == "true" ]]; then
78
+ COST_OVER=$(awk "BEGIN { print (${TOTAL_COST:-0} > ${COST_BUDGET:-1.0}) ? 1 : 0 }" 2>/dev/null) || COST_OVER=0
79
+ [[ "$COST_OVER" == "1" ]] && UNDER_BUDGET="false"
80
+ fi
81
+
82
+ SESSION_PAYLOAD=$(jq -n \
83
+ --argjson total_cost "${TOTAL_COST:-0}" \
84
+ --argjson budget_usd "${COST_BUDGET:-1.0}" \
85
+ --argjson under "$( [[ "$UNDER_BUDGET" == "true" ]] && printf 'true' || printf 'false')" \
86
+ --arg sid "$SESSION_ID" \
87
+ --argjson total_tokens "${TOTAL_TOKENS_INT:-0}" \
88
+ --argjson total_calls "${TOTAL_CALLS:-0}" \
89
+ --argjson dur 0 \
90
+ --argjson calls_blocked 0 \
91
+ --argjson calls_warned 0 \
92
+ --argjson poisoned "$( [[ "$LEDGER_POISONED" == "true" ]] && printf 'true' || printf 'false')" \
93
+ '{
94
+ total_cost_usd: $total_cost,
95
+ budget_usd: $budget_usd,
96
+ under_budget: $under,
97
+ session_id: $sid,
98
+ total_tokens: $total_tokens,
99
+ total_api_calls: $total_calls,
100
+ duration_ms: $dur,
101
+ calls_blocked: $calls_blocked,
102
+ calls_warned: $calls_warned,
103
+ ledger_poisoned: $poisoned
104
+ }' 2>/dev/null) || SESSION_PAYLOAD="{}"
105
+
106
+ governor_emit_event "governor.session.complete" "$SESSION_PAYLOAD" || true
107
+
108
+ _done