@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,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Config resolution for Governor.
|
|
3
|
+
#
|
|
4
|
+
# Reads three layers, latest wins:
|
|
5
|
+
# 1. plugins/governor/config.json (defaults shipped with the plugin)
|
|
6
|
+
# 2. ~/.claude/settings.json
|
|
7
|
+
# 3. <repo>/.claude/settings.json
|
|
8
|
+
#
|
|
9
|
+
# Exposes:
|
|
10
|
+
# governor_config_load <repo_root> # populates _GOVERNOR_CONFIG (JSON)
|
|
11
|
+
# governor_config_get <jq-path> # echoes string value (empty if unset)
|
|
12
|
+
# governor_config_get_json <jq-path> # echoes JSON value (null if unset)
|
|
13
|
+
# governor_config_enabled # 0 if governor.enabled is true
|
|
14
|
+
# governor_config_enforcement # echoes "soft" or "hard"
|
|
15
|
+
|
|
16
|
+
_GOVERNOR_CONFIG="{}"
|
|
17
|
+
|
|
18
|
+
governor_config_load() {
|
|
19
|
+
local repo_root="${1:-}"
|
|
20
|
+
local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
|
|
21
|
+
local home_dir="${HOME:-}"
|
|
22
|
+
|
|
23
|
+
local merged="{}"
|
|
24
|
+
local file
|
|
25
|
+
|
|
26
|
+
file="${plugin_root}/config.json"
|
|
27
|
+
if [[ -f "$file" ]]; then
|
|
28
|
+
local defaults
|
|
29
|
+
defaults=$(jq '.' "$file" 2>/dev/null) || defaults="{}"
|
|
30
|
+
merged=$(jq -n --argjson a "$merged" --argjson b "$defaults" '$a * $b' 2>/dev/null) \
|
|
31
|
+
|| merged="$defaults"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
local repo_settings=""
|
|
35
|
+
[[ -n "$repo_root" ]] && repo_settings="${repo_root}/.claude/settings.json"
|
|
36
|
+
|
|
37
|
+
for file in "${home_dir}/.claude/settings.json" "$repo_settings"; do
|
|
38
|
+
[[ -n "$file" && -f "$file" ]] || continue
|
|
39
|
+
local overlay
|
|
40
|
+
overlay=$(jq '{ governor: (.governor // {}) }' "$file" 2>/dev/null) || continue
|
|
41
|
+
[[ -z "$overlay" ]] && continue
|
|
42
|
+
local attempt
|
|
43
|
+
if attempt=$(jq -n --argjson a "$merged" --argjson b "$overlay" '
|
|
44
|
+
def deepmerge($a; $b):
|
|
45
|
+
if ($a|type) == "object" and ($b|type) == "object" then
|
|
46
|
+
reduce (($a|keys) + ($b|keys) | unique)[] as $k
|
|
47
|
+
({}; .[$k] = deepmerge($a[$k]; $b[$k]))
|
|
48
|
+
elif $b == null then $a
|
|
49
|
+
else $b end;
|
|
50
|
+
deepmerge($a; $b)
|
|
51
|
+
' 2>/dev/null) && [[ -n "$attempt" ]]; then
|
|
52
|
+
merged="$attempt"
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
_GOVERNOR_CONFIG="$merged"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
governor_config_get() {
|
|
60
|
+
local path="$1"
|
|
61
|
+
printf '%s' "$_GOVERNOR_CONFIG" | jq -r "${path} // empty" 2>/dev/null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
governor_config_get_json() {
|
|
65
|
+
local path="$1"
|
|
66
|
+
printf '%s' "$_GOVERNOR_CONFIG" | jq -c "${path}" 2>/dev/null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
governor_config_enabled() {
|
|
70
|
+
local v
|
|
71
|
+
v=$(governor_config_get '.governor.enabled')
|
|
72
|
+
[[ "$v" == "true" ]]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
governor_config_enforcement() {
|
|
76
|
+
local v
|
|
77
|
+
v=$(governor_config_get '.governor.enforcement')
|
|
78
|
+
printf '%s' "${v:-soft}"
|
|
79
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Token estimation for the governor pre-call gate.
|
|
3
|
+
#
|
|
4
|
+
# Uses a tier-table approach: estimate from tool input JSON size with a
|
|
5
|
+
# per-content-type characters-per-token ratio, then multiply by a
|
|
6
|
+
# configurable safety margin.
|
|
7
|
+
#
|
|
8
|
+
# Tier table (characters per token):
|
|
9
|
+
# ASCII prose 4.0
|
|
10
|
+
# code / JSON 3.0
|
|
11
|
+
# mixed 2.5
|
|
12
|
+
# non-Latin 1.5
|
|
13
|
+
#
|
|
14
|
+
# Safety margin (config: governor.estimation.safety_margin, default 1.3)
|
|
15
|
+
# is applied before the gate check. Hard stop margin
|
|
16
|
+
# (governor.estimation.hard_stop_margin, default 1.5) is the threshold
|
|
17
|
+
# for a blocking decision regardless of enforcement mode.
|
|
18
|
+
#
|
|
19
|
+
# Estimation method tag emitted in governor.gate.checked: "tier_table"
|
|
20
|
+
#
|
|
21
|
+
# Exposes:
|
|
22
|
+
# governor_estimate_tokens <json_input> # echoes integer estimate
|
|
23
|
+
# governor_estimate_cost <tokens> <model> # echoes float USD estimate
|
|
24
|
+
# governor_estimate_method # echoes "tier_table"
|
|
25
|
+
|
|
26
|
+
governor_estimate_method() {
|
|
27
|
+
printf 'tier_table'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Detect content tier from a sample of text.
|
|
31
|
+
# Returns one of: ascii_prose code_json mixed non_latin
|
|
32
|
+
_governor_detect_tier() {
|
|
33
|
+
local sample="${1:-}"
|
|
34
|
+
local len=${#sample}
|
|
35
|
+
[[ $len -eq 0 ]] && { printf 'ascii_prose'; return 0; }
|
|
36
|
+
|
|
37
|
+
# Check for structural characters that signal code/JSON
|
|
38
|
+
local struct_count
|
|
39
|
+
struct_count=$(printf '%s' "$sample" | tr -cd '{}[]():;=><' | wc -c 2>/dev/null) \
|
|
40
|
+
|| struct_count=0
|
|
41
|
+
struct_count=$(printf '%s' "$struct_count" | tr -d ' ')
|
|
42
|
+
|
|
43
|
+
# >= 10% structural → code/JSON
|
|
44
|
+
if (( struct_count * 10 >= len )); then
|
|
45
|
+
printf 'code_json'
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Non-ASCII byte presence signals non-Latin
|
|
50
|
+
local ascii_count
|
|
51
|
+
ascii_count=$(printf '%s' "$sample" | tr -cd '[:print:][:space:]' | wc -c 2>/dev/null) \
|
|
52
|
+
|| ascii_count=$len
|
|
53
|
+
ascii_count=$(printf '%s' "$ascii_count" | tr -d ' ')
|
|
54
|
+
|
|
55
|
+
if (( ascii_count * 10 < len * 7 )); then
|
|
56
|
+
printf 'non_latin'
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# 5–9% structural → mixed (prose with embedded code/JSON)
|
|
61
|
+
if (( struct_count * 20 >= len )); then
|
|
62
|
+
printf 'mixed'
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
printf 'ascii_prose'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Estimate token count from a JSON input string.
|
|
70
|
+
# Usage: tokens=$(governor_estimate_tokens "$json_input")
|
|
71
|
+
governor_estimate_tokens() {
|
|
72
|
+
local json_input="${1:-}"
|
|
73
|
+
local safety_margin="${2:-}"
|
|
74
|
+
|
|
75
|
+
[[ -z "$safety_margin" ]] && {
|
|
76
|
+
safety_margin=$(governor_config_get '.governor.estimation.safety_margin' 2>/dev/null)
|
|
77
|
+
safety_margin="${safety_margin:-1.3}"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
local char_count=${#json_input}
|
|
81
|
+
[[ $char_count -eq 0 ]] && { printf '100'; return 0; }
|
|
82
|
+
|
|
83
|
+
local sample="${json_input:0:2000}"
|
|
84
|
+
local tier
|
|
85
|
+
tier=$(_governor_detect_tier "$sample")
|
|
86
|
+
|
|
87
|
+
local chars_per_token
|
|
88
|
+
case "$tier" in
|
|
89
|
+
code_json) chars_per_token="3.0" ;;
|
|
90
|
+
mixed) chars_per_token="2.5" ;;
|
|
91
|
+
non_latin) chars_per_token="1.5" ;;
|
|
92
|
+
*) chars_per_token="4.0" ;;
|
|
93
|
+
esac
|
|
94
|
+
|
|
95
|
+
# Single awk pass for fractional chars_per_token and safety margin
|
|
96
|
+
local tokens
|
|
97
|
+
tokens=$(awk "BEGIN { printf \"%d\", int($char_count / $chars_per_token * $safety_margin + 0.999) }" 2>/dev/null) \
|
|
98
|
+
|| tokens=$(( char_count * 2 ))
|
|
99
|
+
(( tokens < 1 )) && tokens=1
|
|
100
|
+
|
|
101
|
+
printf '%s' "$tokens"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Rough USD cost estimate from token count.
|
|
105
|
+
# Uses Sonnet-class pricing as a conservative default (~$3/M input, $15/M output).
|
|
106
|
+
# governor is not aware of the actual model being spawned, so this is a
|
|
107
|
+
# planning-time upper bound.
|
|
108
|
+
#
|
|
109
|
+
# Usage: cost=$(governor_estimate_cost 5000)
|
|
110
|
+
governor_estimate_cost() {
|
|
111
|
+
local tokens="${1:-0}"
|
|
112
|
+
|
|
113
|
+
# $3 per 1M input + $15 per 1M output; assume 50/50 split → ~$9/M blended
|
|
114
|
+
awk "BEGIN { printf \"%.6f\", ($tokens / 1000000.0) * 9.0 }" 2>/dev/null \
|
|
115
|
+
|| printf '0.0'
|
|
116
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Canonical governor.* event emission.
|
|
3
|
+
#
|
|
4
|
+
# Thin wrapper around the ecosystem plugin's onlooker-event.mjs `emit` mode.
|
|
5
|
+
# Every emission is validated against @onlooker-community/schema v2.4.0+
|
|
6
|
+
# before being appended to ~/.onlooker/logs/onlooker-events.jsonl.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# governor_emit_event "governor.gate.checked" '{"session_id":"...","decision":"allow",...}'
|
|
10
|
+
|
|
11
|
+
_GOVERNOR_PLUGIN_NAME="governor"
|
|
12
|
+
|
|
13
|
+
_governor_event_js_path() {
|
|
14
|
+
if [[ -n "${_ONLOOKER_EVENT_JS:-}" && -f "$_ONLOOKER_EVENT_JS" ]]; then
|
|
15
|
+
printf '%s' "$_ONLOOKER_EVENT_JS"
|
|
16
|
+
return 0
|
|
17
|
+
fi
|
|
18
|
+
local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
|
|
19
|
+
local candidates=(
|
|
20
|
+
"${plugin_root}/scripts/lib/onlooker-event.mjs"
|
|
21
|
+
"${plugin_root}/../../scripts/lib/onlooker-event.mjs"
|
|
22
|
+
)
|
|
23
|
+
local c
|
|
24
|
+
for c in "${candidates[@]}"; do
|
|
25
|
+
[[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
|
|
26
|
+
done
|
|
27
|
+
return 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_governor_session_id() {
|
|
31
|
+
if [[ -n "${_HOOK_SESSION_ID:-}" ]]; then
|
|
32
|
+
printf '%s' "$_HOOK_SESSION_ID"
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
if [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then
|
|
36
|
+
printf '%s' "$CLAUDE_SESSION_ID"
|
|
37
|
+
return 0
|
|
38
|
+
fi
|
|
39
|
+
printf 'unknown'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Emit a single governor.* event. Returns 0 on success, non-zero on failure.
|
|
43
|
+
governor_emit_event() {
|
|
44
|
+
local event_type="${1:-}"
|
|
45
|
+
local payload="${2:-}"
|
|
46
|
+
|
|
47
|
+
[[ -z "$event_type" || -z "$payload" ]] && return 1
|
|
48
|
+
|
|
49
|
+
local event_js
|
|
50
|
+
event_js=$(_governor_event_js_path) || return 1
|
|
51
|
+
|
|
52
|
+
local session_id
|
|
53
|
+
session_id=$(_governor_session_id)
|
|
54
|
+
|
|
55
|
+
local params
|
|
56
|
+
params=$(jq -n \
|
|
57
|
+
--arg plugin "$_GOVERNOR_PLUGIN_NAME" \
|
|
58
|
+
--arg sid "$session_id" \
|
|
59
|
+
--arg type "$event_type" \
|
|
60
|
+
--argjson payload "$payload" \
|
|
61
|
+
'{plugin: $plugin, session_id: $sid, event_type: $type, payload: $payload}' \
|
|
62
|
+
2>/dev/null) || return 1
|
|
63
|
+
|
|
64
|
+
local event
|
|
65
|
+
local stderr_file
|
|
66
|
+
stderr_file=$(mktemp -t governor-event-err.XXXXXX 2>/dev/null) || stderr_file="/tmp/governor-event-err.$$"
|
|
67
|
+
event=$(printf '%s' "$params" \
|
|
68
|
+
| ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}" \
|
|
69
|
+
ONLOOKER_PLUGIN_NAME="$_GOVERNOR_PLUGIN_NAME" \
|
|
70
|
+
node "$event_js" emit 2>"$stderr_file") || {
|
|
71
|
+
printf 'governor_emit_event: schema validation failed for %s\n' "$event_type" >&2
|
|
72
|
+
[[ -s "$stderr_file" ]] && cat "$stderr_file" >&2
|
|
73
|
+
rm -f "$stderr_file"
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
rm -f "$stderr_file"
|
|
77
|
+
|
|
78
|
+
local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
|
|
79
|
+
mkdir -p "$(dirname "$log_path")" 2>/dev/null || return 1
|
|
80
|
+
printf '%s\n' "$event" >> "$log_path"
|
|
81
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# JSONL ledger read/write for the governor plugin.
|
|
3
|
+
#
|
|
4
|
+
# Two-phase accounting model:
|
|
5
|
+
#
|
|
6
|
+
# 1. PreToolUse (gate): writes a "reservation" record inside the gate
|
|
7
|
+
# lock with estimated_tokens > 0. This ensures concurrent spawns
|
|
8
|
+
# each see the others' in-flight cost before deciding to allow.
|
|
9
|
+
#
|
|
10
|
+
# 2. PostToolUse (completion): writes a "Task" record with
|
|
11
|
+
# estimated_tokens = -(original estimate) to cancel the reservation,
|
|
12
|
+
# plus actual_tokens = observed count (if available). Net effect:
|
|
13
|
+
#
|
|
14
|
+
# in-flight: estimated_tokens(reservation) = N_est
|
|
15
|
+
# completed: estimated_tokens(Task) = -N_est, actual_tokens = N_act
|
|
16
|
+
# total: N_est + (-N_est) + N_act = N_act ✓
|
|
17
|
+
#
|
|
18
|
+
# governor_ledger_total_tokens sums (.estimated_tokens + .actual_tokens)
|
|
19
|
+
# across all records so the running total is always:
|
|
20
|
+
# (in-flight reservation estimates) + (completed actuals)
|
|
21
|
+
#
|
|
22
|
+
# Requires portable-lock.sh to be sourced beforehand.
|
|
23
|
+
|
|
24
|
+
GOVERNOR_LEDGER_MAX_RETRIES=3
|
|
25
|
+
GOVERNOR_LEDGER_LOCK_TIMEOUT=5
|
|
26
|
+
|
|
27
|
+
governor_ledger_path() {
|
|
28
|
+
local session_id="${1:-unknown}"
|
|
29
|
+
local dir="${ONLOOKER_DIR:-${HOME}/.onlooker}/governance/ledgers"
|
|
30
|
+
local safe_id
|
|
31
|
+
safe_id=$(printf '%s' "$session_id" | tr -c 'a-zA-Z0-9-' '_')
|
|
32
|
+
printf '%s/%s.jsonl' "$dir" "$safe_id"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
governor_ledger_poison_path() {
|
|
36
|
+
local ledger_path="${1:-}"
|
|
37
|
+
printf '%s.poisoned' "$ledger_path"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
governor_ledger_is_poisoned() {
|
|
41
|
+
local session_id="${1:-}"
|
|
42
|
+
local ledger_path
|
|
43
|
+
ledger_path=$(governor_ledger_path "$session_id")
|
|
44
|
+
[[ -f "$(governor_ledger_poison_path "$ledger_path")" ]]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Append a record to the ledger under the ledger's own write lock.
|
|
48
|
+
# Safe to call from PostToolUse and other hooks that do not already hold
|
|
49
|
+
# the gate lock. For writing inside the gate lock use governor_ledger_write_direct.
|
|
50
|
+
#
|
|
51
|
+
# Usage: governor_ledger_append "$session_id" "$record_json"
|
|
52
|
+
governor_ledger_append() {
|
|
53
|
+
local session_id="${1:-}"
|
|
54
|
+
local record="${2:-}"
|
|
55
|
+
|
|
56
|
+
[[ -z "$session_id" || -z "$record" ]] && return 1
|
|
57
|
+
|
|
58
|
+
local ledger_path
|
|
59
|
+
ledger_path=$(governor_ledger_path "$session_id")
|
|
60
|
+
local lock_path="${ledger_path}.lock"
|
|
61
|
+
|
|
62
|
+
mkdir -p "$(dirname "$ledger_path")" 2>/dev/null || return 1
|
|
63
|
+
|
|
64
|
+
local attempt=0
|
|
65
|
+
local unrecorded_tokens=0
|
|
66
|
+
unrecorded_tokens=$(printf '%s' "$record" | jq -r '.estimated_tokens // 0' 2>/dev/null) \
|
|
67
|
+
|| unrecorded_tokens=0
|
|
68
|
+
|
|
69
|
+
while (( attempt < GOVERNOR_LEDGER_MAX_RETRIES )); do
|
|
70
|
+
if lock_acquire "$lock_path" "$GOVERNOR_LEDGER_LOCK_TIMEOUT"; then
|
|
71
|
+
printf '%s\n' "$(printf '%s' "$record" | jq -c . 2>/dev/null)" >> "$ledger_path" 2>/dev/null
|
|
72
|
+
local write_ok=$?
|
|
73
|
+
lock_release "$lock_path"
|
|
74
|
+
if (( write_ok == 0 )); then
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
attempt=$(( attempt + 1 ))
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
_governor_ledger_poison "$session_id" "$ledger_path" "$unrecorded_tokens"
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Write a record directly to the ledger file without acquiring the write
|
|
86
|
+
# lock. ONLY call this when you already hold the gate lock, which serializes
|
|
87
|
+
# access. The gate lock is the same as the write lock (same .lock path), so
|
|
88
|
+
# re-acquiring it here would deadlock.
|
|
89
|
+
#
|
|
90
|
+
# Usage: governor_ledger_write_direct "$ledger_path" "$record_json"
|
|
91
|
+
governor_ledger_write_direct() {
|
|
92
|
+
local ledger_path="${1:-}"
|
|
93
|
+
local record="${2:-}"
|
|
94
|
+
|
|
95
|
+
[[ -z "$ledger_path" || -z "$record" ]] && return 1
|
|
96
|
+
|
|
97
|
+
mkdir -p "$(dirname "$ledger_path")" 2>/dev/null || return 1
|
|
98
|
+
printf '%s\n' "$(printf '%s' "$record" | jq -c . 2>/dev/null)" >> "$ledger_path" 2>/dev/null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_governor_ledger_poison() {
|
|
102
|
+
local session_id="${1:-}"
|
|
103
|
+
local ledger_path="${2:-}"
|
|
104
|
+
local unrecorded_tokens="${3:-0}"
|
|
105
|
+
|
|
106
|
+
touch "$(governor_ledger_poison_path "$ledger_path")" 2>/dev/null || true
|
|
107
|
+
|
|
108
|
+
local poison_payload
|
|
109
|
+
poison_payload=$(jq -n \
|
|
110
|
+
--arg sid "$session_id" \
|
|
111
|
+
--arg aid "${CLAUDE_SESSION_ID:-unknown}" \
|
|
112
|
+
--arg err "write failed after ${GOVERNOR_LEDGER_MAX_RETRIES} attempts" \
|
|
113
|
+
--argjson retries "$GOVERNOR_LEDGER_MAX_RETRIES" \
|
|
114
|
+
--argjson tok "$unrecorded_tokens" \
|
|
115
|
+
'{
|
|
116
|
+
session_id: $sid,
|
|
117
|
+
agent_id: $aid,
|
|
118
|
+
error: $err,
|
|
119
|
+
retry_count: $retries,
|
|
120
|
+
ledger_poisoned: true,
|
|
121
|
+
unrecorded_tokens: $tok
|
|
122
|
+
}' 2>/dev/null) || poison_payload="{}"
|
|
123
|
+
|
|
124
|
+
governor_emit_event "governor.ledger.write_failed" "$poison_payload" || true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Running total of tokens for a session.
|
|
128
|
+
#
|
|
129
|
+
# Uses the two-phase model: each record contributes
|
|
130
|
+
# .estimated_tokens + (.actual_tokens // 0)
|
|
131
|
+
#
|
|
132
|
+
# In-flight reservations: estimated_tokens > 0, no actual_tokens → counts N_est
|
|
133
|
+
# Completed Task records: estimated_tokens = -N_est, actual_tokens = N_act → counts N_act
|
|
134
|
+
# Net: in-flight estimates + completed actuals.
|
|
135
|
+
#
|
|
136
|
+
# Usage: tokens=$(governor_ledger_total_tokens "$session_id")
|
|
137
|
+
governor_ledger_total_tokens() {
|
|
138
|
+
local session_id="${1:-}"
|
|
139
|
+
local ledger_path
|
|
140
|
+
ledger_path=$(governor_ledger_path "$session_id")
|
|
141
|
+
|
|
142
|
+
[[ -f "$ledger_path" ]] || { printf '0'; return 0; }
|
|
143
|
+
|
|
144
|
+
jq -s '[.[] | ((.estimated_tokens // 0) + (.actual_tokens // 0))] | add // 0' \
|
|
145
|
+
"$ledger_path" 2>/dev/null || printf '0'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Running total of cost for a session (same two-phase logic as tokens).
|
|
149
|
+
# Usage: cost=$(governor_ledger_total_cost "$session_id")
|
|
150
|
+
governor_ledger_total_cost() {
|
|
151
|
+
local session_id="${1:-}"
|
|
152
|
+
local ledger_path
|
|
153
|
+
ledger_path=$(governor_ledger_path "$session_id")
|
|
154
|
+
|
|
155
|
+
[[ -f "$ledger_path" ]] || { printf '0'; return 0; }
|
|
156
|
+
|
|
157
|
+
jq -s '[.[] | ((.cost_usd_estimated // 0) + (.cost_usd_actual // 0))] | add // 0' \
|
|
158
|
+
"$ledger_path" 2>/dev/null || printf '0'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Count completed Task calls (excludes reservation records).
|
|
162
|
+
# Usage: calls=$(governor_ledger_call_count "$session_id")
|
|
163
|
+
governor_ledger_call_count() {
|
|
164
|
+
local session_id="${1:-}"
|
|
165
|
+
local ledger_path
|
|
166
|
+
ledger_path=$(governor_ledger_path "$session_id")
|
|
167
|
+
|
|
168
|
+
[[ -f "$ledger_path" ]] || { printf '0'; return 0; }
|
|
169
|
+
|
|
170
|
+
jq -s '[.[] | select(.agent_type == "Task" and .record_type != "reservation")] | length' \
|
|
171
|
+
"$ledger_path" 2>/dev/null || printf '0'
|
|
172
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scribe",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Intent documentation from agent activity. Captures why changes were made — problem context, decisions, tradeoffs — and distills them into readable artifacts at session end.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Onlooker Community",
|
|
7
|
+
"email": "community@onlooker.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://onlooker.dev",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"requires": ["ecosystem"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/onlooker-community/ecosystem/compare/scribe-v0.1.0...scribe-v0.2.0) (2026-06-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **scribe:** intent documentation from agent activity :pencil2: ([#50](https://github.com/onlooker-community/ecosystem/issues/50)) ([f0a95d1](https://github.com/onlooker-community/ecosystem/commit/f0a95d1058e36d1bb5f0f645964d9e88e8f98b66))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scribe": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"evaluator": {
|
|
5
|
+
"model": "claude-haiku-4-5-20251001",
|
|
6
|
+
"timeout": 60,
|
|
7
|
+
"max_tokens": 2048,
|
|
8
|
+
"temperature": 0.3
|
|
9
|
+
},
|
|
10
|
+
"capture": {
|
|
11
|
+
"min_turns": 3,
|
|
12
|
+
"prompt_max_chars": 1000,
|
|
13
|
+
"transcript_chars_max": 40000
|
|
14
|
+
},
|
|
15
|
+
"output": {
|
|
16
|
+
"mirror_to_project": false,
|
|
17
|
+
"project_dir": "docs/decisions"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/scribe-session-start.sh"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"UserPromptSubmit": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "*",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/scribe-capture.sh"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"Stop": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "*",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/scribe-stop.sh"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scribe UserPromptSubmit hook — initial intent capture.
|
|
3
|
+
#
|
|
4
|
+
# Fires on every user prompt. On the FIRST turn of a session (captured_prompt
|
|
5
|
+
# is null in state), extracts and stores the prompt text as the problem
|
|
6
|
+
# statement seed for later distillation.
|
|
7
|
+
#
|
|
8
|
+
# Subsequent turns are ignored — the full transcript is available at Stop
|
|
9
|
+
# time, so there is no need to accumulate per-turn captures.
|
|
10
|
+
#
|
|
11
|
+
# Hook contract:
|
|
12
|
+
# - Always exits 0. Never blocks a user prompt.
|
|
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
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
21
|
+
|
|
22
|
+
# shellcheck source=../lib/scribe-config.sh
|
|
23
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-config.sh"
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
|
|
27
|
+
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
|
|
28
|
+
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // ""' 2>/dev/null) || PROMPT=""
|
|
29
|
+
|
|
30
|
+
_done() { exit 0; }
|
|
31
|
+
|
|
32
|
+
[[ -z "$SESSION_ID" || -z "$PROMPT" ]] && _done
|
|
33
|
+
|
|
34
|
+
scribe_config_load "$CWD"
|
|
35
|
+
|
|
36
|
+
if ! scribe_config_enabled; then
|
|
37
|
+
_done
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
ONLOOKER_DIR="${ONLOOKER_DIR:-${HOME}/.onlooker}"
|
|
41
|
+
STATE_FILE="${ONLOOKER_DIR}/scribe/sessions/${SESSION_ID}.json"
|
|
42
|
+
|
|
43
|
+
# Only capture if no prompt has been stored yet for this session.
|
|
44
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
45
|
+
existing=$(jq -r '.captured_prompt // "null"' "$STATE_FILE" 2>/dev/null) || existing="null"
|
|
46
|
+
[[ "$existing" != "null" && -n "$existing" ]] && _done
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Truncate prompt to configured max chars.
|
|
50
|
+
max_chars=$(scribe_config_get '.scribe.capture.prompt_max_chars')
|
|
51
|
+
[[ -z "$max_chars" || "$max_chars" == "null" ]] && max_chars="1000"
|
|
52
|
+
|
|
53
|
+
truncated="${PROMPT:0:$max_chars}"
|
|
54
|
+
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || timestamp=""
|
|
55
|
+
|
|
56
|
+
# Upsert state — preserve existing keys, update captured_prompt + captured_at.
|
|
57
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
58
|
+
updated=$(jq \
|
|
59
|
+
--arg p "$truncated" \
|
|
60
|
+
--arg ts "$timestamp" \
|
|
61
|
+
'.captured_prompt = $p | .captured_at = $ts' \
|
|
62
|
+
"$STATE_FILE" 2>/dev/null) || updated=""
|
|
63
|
+
if [[ -n "$updated" ]]; then
|
|
64
|
+
printf '%s\n' "$updated" > "$STATE_FILE" || true
|
|
65
|
+
fi
|
|
66
|
+
else
|
|
67
|
+
mkdir -p "$(dirname "$STATE_FILE")" 2>/dev/null || true
|
|
68
|
+
jq -n \
|
|
69
|
+
--arg sid "$SESSION_ID" \
|
|
70
|
+
--arg p "$truncated" \
|
|
71
|
+
--arg ts "$timestamp" \
|
|
72
|
+
'{session_id: $sid, captured_prompt: $p, captured_at: $ts}' \
|
|
73
|
+
2>/dev/null > "$STATE_FILE" || true
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
_done
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scribe SessionStart hook.
|
|
3
|
+
#
|
|
4
|
+
# Fires at every session start. Responsibilities:
|
|
5
|
+
# 1. Skip silently when scribe.enabled is false.
|
|
6
|
+
# 2. Create storage directories.
|
|
7
|
+
# 3. Initialize session state file:
|
|
8
|
+
# - captured_prompt: null (populated by scribe-capture.sh on first turn)
|
|
9
|
+
# - captured_at: null
|
|
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
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
21
|
+
|
|
22
|
+
# shellcheck source=../lib/scribe-config.sh
|
|
23
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-config.sh"
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
|
|
27
|
+
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
|
|
28
|
+
|
|
29
|
+
_done() { exit 0; }
|
|
30
|
+
|
|
31
|
+
scribe_config_load "$CWD"
|
|
32
|
+
|
|
33
|
+
if ! scribe_config_enabled; then
|
|
34
|
+
_done
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
export _HOOK_SESSION_ID="$SESSION_ID"
|
|
38
|
+
|
|
39
|
+
ONLOOKER_DIR="${ONLOOKER_DIR:-${HOME}/.onlooker}"
|
|
40
|
+
SCRIBE_SESSION_DIR="${ONLOOKER_DIR}/scribe/sessions"
|
|
41
|
+
mkdir -p "$SCRIBE_SESSION_DIR" 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
[[ -z "$SESSION_ID" ]] && _done
|
|
44
|
+
|
|
45
|
+
STATE_FILE="${SCRIBE_SESSION_DIR}/${SESSION_ID}.json"
|
|
46
|
+
|
|
47
|
+
jq -n \
|
|
48
|
+
--arg sid "$SESSION_ID" \
|
|
49
|
+
'{
|
|
50
|
+
session_id: $sid,
|
|
51
|
+
captured_prompt: null,
|
|
52
|
+
captured_at: null
|
|
53
|
+
}' 2>/dev/null > "$STATE_FILE" || {
|
|
54
|
+
printf 'scribe-session-start: failed to write state file %s\n' "$STATE_FILE" >&2
|
|
55
|
+
_done
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_done
|