@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,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