@onlooker-community/ecosystem 0.16.0 → 0.18.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 +2 -2
  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/counsel/.claude-plugin/plugin.json +14 -0
  24. package/plugins/counsel/CHANGELOG.md +8 -0
  25. package/plugins/counsel/config.json +20 -0
  26. package/plugins/counsel/hooks/hooks.json +15 -0
  27. package/plugins/counsel/scripts/hooks/counsel-session-start.sh +106 -0
  28. package/plugins/counsel/scripts/lib/counsel-brief.sh +247 -0
  29. package/plugins/counsel/scripts/lib/counsel-config.sh +72 -0
  30. package/plugins/counsel/scripts/lib/counsel-events.sh +80 -0
  31. package/plugins/counsel/scripts/lib/counsel-project-key.sh +79 -0
  32. package/plugins/counsel/scripts/lib/counsel-reader.sh +114 -0
  33. package/plugins/counsel/scripts/lib/counsel-synthesize.sh +103 -0
  34. package/plugins/counsel/scripts/lib/counsel-ulid.sh +45 -0
  35. package/plugins/governor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/governor/CHANGELOG.md +7 -0
  37. package/plugins/scribe/.claude-plugin/plugin.json +12 -0
  38. package/plugins/scribe/CHANGELOG.md +8 -0
  39. package/plugins/scribe/config.json +20 -0
  40. package/plugins/scribe/hooks/hooks.json +37 -0
  41. package/plugins/scribe/scripts/hooks/scribe-capture.sh +76 -0
  42. package/plugins/scribe/scripts/hooks/scribe-session-start.sh +58 -0
  43. package/plugins/scribe/scripts/hooks/scribe-stop.sh +67 -0
  44. package/plugins/scribe/scripts/lib/scribe-config.sh +72 -0
  45. package/plugins/scribe/scripts/lib/scribe-distill.sh +239 -0
  46. package/plugins/scribe/scripts/lib/scribe-events.sh +80 -0
  47. package/plugins/scribe/scripts/lib/scribe-extract.sh +147 -0
  48. package/plugins/scribe/scripts/lib/scribe-project-key.sh +89 -0
  49. package/plugins/scribe/scripts/lib/scribe-ulid.sh +50 -0
  50. package/release-please-config.json +48 -0
  51. package/test/bats/counsel-project-key.bats +82 -0
  52. package/test/bats/counsel-reader.bats +132 -0
  53. package/test/bats/scribe-extract.bats +102 -0
  54. package/test/bats/scribe-project-key.bats +75 -0
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # Event log reader for Counsel.
3
+ #
4
+ # Reads $ONLOOKER_EVENTS_LOG and returns a filtered, summarized view of
5
+ # the last N days suitable for passing to the synthesis prompt.
6
+ #
7
+ # Exposes:
8
+ # counsel_read_events <lookback_days> <chars_max>
9
+ # Echoes a structured text summary of events, or empty string on failure.
10
+ #
11
+ # counsel_sources_from_events <events_json>
12
+ # Echoes a JSON array of CounselSource strings present in the event batch.
13
+
14
+ # Maps event_type prefixes to CounselSource values.
15
+ _counsel_source_for_type() {
16
+ local event_type="${1:-}"
17
+ case "$event_type" in
18
+ tribunal.*) printf 'tribunal_verdicts' ;;
19
+ echo.*) printf 'echo_regressions' ;;
20
+ sentinel.*) printf 'sentinel_audit' ;;
21
+ warden.*) printf 'warden_audit' ;;
22
+ oracle.*) printf 'oracle_calibrations' ;;
23
+ meridian.*) printf 'meridian_reliance' ;;
24
+ *) printf 'onlooker_events' ;;
25
+ esac
26
+ }
27
+
28
+ counsel_read_events() {
29
+ local lookback_days="${1:-30}"
30
+ local chars_max="${2:-60000}"
31
+
32
+ local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
33
+ [[ -f "$log_path" ]] || { printf ''; return 0; }
34
+
35
+ # Compute cutoff as an ISO 8601 date string. ISO 8601 strings are
36
+ # lexicographically sortable, so string comparison is safe for filtering.
37
+ local cutoff_ts
38
+ if [[ "$(uname)" == "Darwin" ]]; then
39
+ cutoff_ts=$(date -v "-${lookback_days}d" -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
40
+ || cutoff_ts=""
41
+ else
42
+ cutoff_ts=$(date -d "-${lookback_days} days" -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) \
43
+ || cutoff_ts=""
44
+ fi
45
+
46
+ # Filter to events within the lookback window. If cutoff_ts is empty (date
47
+ # command unavailable) fall through and include all events.
48
+ local summary
49
+ # -rc: compact output keeps each object on one line (JSONL-shaped), which
50
+ # downstream counsel_count_events and counsel_sources_from_events require.
51
+ summary=$(jq -rc --arg cutoff "$cutoff_ts" '
52
+ select(.timestamp != null) |
53
+ select($cutoff == "" or .timestamp >= $cutoff) |
54
+ {
55
+ type: .event_type,
56
+ plugin: (.plugin // "unknown"),
57
+ ts: .timestamp,
58
+ session: (.session_id // ""),
59
+ payload: (.payload // {})
60
+ }
61
+ ' "$log_path" 2>/dev/null | head -c "$chars_max") || summary=""
62
+
63
+ printf '%s' "$summary"
64
+ }
65
+
66
+ counsel_sources_from_events() {
67
+ local events_text="${1:-}"
68
+ [[ -z "$events_text" ]] && { printf '["onlooker_events"]'; return 0; }
69
+
70
+ local sources=()
71
+ local seen_tribunal=0 seen_echo=0 seen_sentinel=0 seen_warden=0 seen_oracle=0 seen_meridian=0 seen_other=0
72
+
73
+ while IFS= read -r line; do
74
+ [[ -z "$line" ]] && continue
75
+ local etype
76
+ etype=$(printf '%s' "$line" | jq -r '.type // ""' 2>/dev/null) || continue
77
+ case "$etype" in
78
+ tribunal.*) seen_tribunal=1 ;;
79
+ echo.*) seen_echo=1 ;;
80
+ sentinel.*) seen_sentinel=1 ;;
81
+ warden.*) seen_warden=1 ;;
82
+ oracle.*) seen_oracle=1 ;;
83
+ meridian.*) seen_meridian=1 ;;
84
+ *) seen_other=1 ;;
85
+ esac
86
+ done <<< "$events_text"
87
+
88
+ [[ "$seen_other" -eq 1 ]] && sources+=("\"onlooker_events\"")
89
+ [[ "$seen_tribunal" -eq 1 ]] && sources+=("\"tribunal_verdicts\"")
90
+ [[ "$seen_echo" -eq 1 ]] && sources+=("\"echo_regressions\"")
91
+ [[ "$seen_sentinel" -eq 1 ]] && sources+=("\"sentinel_audit\"")
92
+ [[ "$seen_warden" -eq 1 ]] && sources+=("\"warden_audit\"")
93
+ [[ "$seen_oracle" -eq 1 ]] && sources+=("\"oracle_calibrations\"")
94
+ [[ "$seen_meridian" -eq 1 ]] && sources+=("\"meridian_reliance\"")
95
+
96
+ if [[ "${#sources[@]}" -eq 0 ]]; then
97
+ printf '["onlooker_events"]'
98
+ return 0
99
+ fi
100
+
101
+ local joined
102
+ joined=$(IFS=,; printf '%s' "${sources[*]}")
103
+ printf '[%s]' "$joined"
104
+ }
105
+
106
+ counsel_count_events() {
107
+ local events_text="${1:-}"
108
+ [[ -z "$events_text" ]] && { printf '0'; return 0; }
109
+ local count=0
110
+ while IFS= read -r line; do
111
+ [[ -n "$line" ]] && count=$((count + 1))
112
+ done <<< "$events_text"
113
+ printf '%s' "$count"
114
+ }
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ # Synthesis pass for Counsel.
3
+ #
4
+ # Runs a single Haiku call over the event summary to produce a structured
5
+ # improvement brief. The brief is returned as a JSON object.
6
+ #
7
+ # Exposes:
8
+ # counsel_synthesize <events_text> <model> <timeout_s> <max_tokens> <temperature>
9
+ # Echoes a JSON object on success, empty string on failure.
10
+ # JSON shape:
11
+ # {
12
+ # "summary": string,
13
+ # "patterns": [string],
14
+ # "recommendations": [{title, rationale, priority:"high"|"medium"|"low"}],
15
+ # "wins": [string],
16
+ # "watch": [string]
17
+ # }
18
+
19
+ _COUNSEL_SYNTHESIS_PROMPT='You are an engineering coach analyzing an AI agent observability log. You have been given a structured dump of plugin events from the onlooker ecosystem over the past several weeks. Your job is to synthesize patterns, surface improvement opportunities, and highlight what is working well.
20
+
21
+ Focus on:
22
+ - Recurring failure modes or blocked gates (tribunal, sentinel, warden)
23
+ - Prompt regression trends (echo plugin)
24
+ - Budget or resource pressure patterns (governor plugin)
25
+ - Quality trends over time
26
+ - What the team is consistently doing well
27
+
28
+ Return a JSON object with exactly these keys:
29
+ {
30
+ "summary": "2-3 sentence executive summary of the period",
31
+ "patterns": ["observed pattern — what is happening and how often"],
32
+ "recommendations": [
33
+ {
34
+ "title": "short action title",
35
+ "rationale": "1-2 sentences explaining why this matters",
36
+ "priority": "high"
37
+ }
38
+ ],
39
+ "wins": ["thing that is working well — be specific"],
40
+ "watch": ["trend to monitor — not urgent but worth watching"]
41
+ }
42
+
43
+ Rules:
44
+ - All fields are required; use empty arrays [] if no items found
45
+ - recommendations must have priority: "high", "medium", or "low"
46
+ - Keep each item to 1-2 sentences
47
+ - Return ONLY the JSON object — no prose, no markdown fences, no explanation
48
+ - If there is insufficient data to draw conclusions, say so in summary and return empty arrays
49
+
50
+ '
51
+
52
+ counsel_synthesize() {
53
+ local events_text="${1:-}"
54
+ local model="${2:-claude-haiku-4-5-20251001}"
55
+ local timeout_s="${3:-90}"
56
+ local max_tokens="${4:-4096}"
57
+ local temperature="${5:-0.4}"
58
+
59
+ [[ -z "$events_text" ]] && return 1
60
+
61
+ if ! command -v claude >/dev/null 2>&1; then
62
+ printf 'counsel_synthesize: claude CLI not found\n' >&2
63
+ return 1
64
+ fi
65
+
66
+ local prompt_file
67
+ prompt_file=$(mktemp -t counsel-synth.XXXXXX 2>/dev/null) || prompt_file="/tmp/counsel-synth.$$"
68
+ trap 'rm -f "$prompt_file"' RETURN
69
+
70
+ {
71
+ printf '%s' "$_COUNSEL_SYNTHESIS_PROMPT"
72
+ printf '<event_log>\n'
73
+ printf '%s\n' "$events_text"
74
+ printf '</event_log>\n'
75
+ } > "$prompt_file"
76
+
77
+ local claude_args=(-p --max-turns 1 --model "$model" --max-tokens "$max_tokens")
78
+
79
+ local response=""
80
+ if command -v timeout >/dev/null 2>&1; then
81
+ response=$(timeout "$timeout_s" claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
82
+ elif command -v gtimeout >/dev/null 2>&1; then
83
+ response=$(gtimeout "$timeout_s" claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
84
+ else
85
+ response=$(claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
86
+ fi
87
+
88
+ [[ -z "$response" ]] && return 1
89
+
90
+ # Strip markdown fences if present.
91
+ local clean
92
+ clean=$(printf '%s' "$response" \
93
+ | sed -e 's/^```json[[:space:]]*//' -e 's/^```[[:space:]]*//' -e 's/[[:space:]]*```$//')
94
+
95
+ if ! printf '%s' "$clean" | jq -e \
96
+ '.summary and (.patterns | type == "array") and (.recommendations | type == "array") and (.wins | type == "array") and (.watch | type == "array")' \
97
+ >/dev/null 2>&1; then
98
+ printf 'counsel_synthesize: response missing required keys\n' >&2
99
+ return 1
100
+ fi
101
+
102
+ printf '%s' "$clean"
103
+ }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # Minimal ULID generator for Counsel brief IDs.
3
+
4
+ _COUNSEL_ULID_ALPHABET="0123456789ABCDEFGHJKMNPQRSTVWXYZ"
5
+
6
+ _counsel_ulid_encode() {
7
+ local n="$1"
8
+ local len="$2"
9
+ local out=""
10
+ local i
11
+ for ((i = 0; i < len; i++)); do
12
+ out="${_COUNSEL_ULID_ALPHABET:$((n % 32)):1}${out}"
13
+ n=$((n / 32))
14
+ done
15
+ printf '%s' "$out"
16
+ }
17
+
18
+ counsel_ulid() {
19
+ local now_ms
20
+ if [[ "$(uname)" == "Darwin" ]]; then
21
+ now_ms=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null) \
22
+ || now_ms=$(($(date +%s) * 1000))
23
+ else
24
+ now_ms=$(date +%s%3N 2>/dev/null) || now_ms=$(($(date +%s) * 1000))
25
+ fi
26
+
27
+ local rand_hex rand_hi rand_lo
28
+ rand_hex=$(openssl rand -hex 10 2>/dev/null)
29
+ if [[ -n "$rand_hex" && ${#rand_hex} -eq 20 ]]; then
30
+ rand_hi=$((16#${rand_hex:0:10}))
31
+ rand_lo=$((16#${rand_hex:10:10}))
32
+ else
33
+ rand_hi=$((RANDOM * 32768 + RANDOM))
34
+ rand_lo=$((RANDOM * 32768 + RANDOM))
35
+ rand_hi=$(((rand_hi * 256 + RANDOM % 256) & ((1 << 40) - 1)))
36
+ rand_lo=$(((rand_lo * 256 + RANDOM % 256) & ((1 << 40) - 1)))
37
+ fi
38
+
39
+ local ts_part hi_part lo_part
40
+ ts_part=$(_counsel_ulid_encode "$now_ms" 10)
41
+ hi_part=$(_counsel_ulid_encode "$rand_hi" 8)
42
+ lo_part=$(_counsel_ulid_encode "$rand_lo" 8)
43
+
44
+ printf '%s%s%s' "$ts_part" "$hi_part" "$lo_part"
45
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "governor",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
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
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
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
+
3
10
  ## [0.1.0] - 2026-05-25
4
11
 
5
12
  ### Added
@@ -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
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # Scribe Stop hook — intent distillation.
3
+ #
4
+ # Fires when the agent session ends. Reads the full session transcript,
5
+ # runs a Haiku extraction pass to identify the problem context, decisions,
6
+ # tradeoffs, and constraints, then writes a Markdown intent document to
7
+ # ~/.onlooker/scribe/<project_key>/<date>-<session>.md.
8
+ #
9
+ # Skip conditions (all silent):
10
+ # - scribe.enabled is false
11
+ # - no transcript_path in hook input, or file is unreadable
12
+ # - session has fewer turns than scribe.capture.min_turns
13
+ #
14
+ # Hook contract:
15
+ # - Always exits 0. Never blocks Stop.
16
+ # - Errors are written to stderr only; stdout is kept clean.
17
+
18
+ set -uo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
22
+
23
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
24
+
25
+ # shellcheck source=../lib/scribe-config.sh
26
+ source "${PLUGIN_ROOT}/scripts/lib/scribe-config.sh"
27
+ # shellcheck source=../lib/scribe-events.sh
28
+ source "${PLUGIN_ROOT}/scripts/lib/scribe-events.sh"
29
+ # shellcheck source=../lib/scribe-project-key.sh
30
+ source "${PLUGIN_ROOT}/scripts/lib/scribe-project-key.sh"
31
+ # shellcheck source=../lib/scribe-extract.sh
32
+ source "${PLUGIN_ROOT}/scripts/lib/scribe-extract.sh"
33
+ # shellcheck source=../lib/scribe-distill.sh
34
+ source "${PLUGIN_ROOT}/scripts/lib/scribe-distill.sh"
35
+
36
+ INPUT=$(cat)
37
+ SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
38
+ CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
39
+ TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null) || TRANSCRIPT_PATH=""
40
+
41
+ export _HOOK_SESSION_ID="$SESSION_ID"
42
+
43
+ _done() { exit 0; }
44
+
45
+ [[ -z "$SESSION_ID" ]] && _done
46
+
47
+ scribe_config_load "$CWD"
48
+
49
+ if ! scribe_config_enabled; then
50
+ _done
51
+ fi
52
+
53
+ if [[ -z "$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]]; then
54
+ _done
55
+ fi
56
+
57
+ _distill_rc=0
58
+ output_path=$(scribe_distill "$SESSION_ID" "$CWD" "$TRANSCRIPT_PATH") || _distill_rc=$?
59
+ if [[ $_distill_rc -ne 0 ]]; then
60
+ # rc=2 means below min_turns — silent skip, not an error.
61
+ [[ $_distill_rc -ne 2 ]] && printf 'scribe-stop: distillation failed for session %s\n' "$SESSION_ID" >&2
62
+ _done
63
+ fi
64
+
65
+ [[ -n "$output_path" ]] && printf 'scribe: intent document written → %s\n' "$output_path" >&2
66
+
67
+ _done
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Config resolution for Scribe.
3
+ #
4
+ # Reads three layers, latest wins:
5
+ # 1. plugins/scribe/config.json (defaults shipped with the plugin)
6
+ # 2. ~/.claude/settings.json
7
+ # 3. <repo>/.claude/settings.json
8
+ #
9
+ # Exposes:
10
+ # scribe_config_load <repo_root> # populates _SCRIBE_CONFIG (JSON)
11
+ # scribe_config_get <jq-path> # echoes string value (empty if unset)
12
+ # scribe_config_get_json <jq-path> # echoes JSON value (null if unset)
13
+ # scribe_config_enabled # 0 if scribe.enabled is true
14
+
15
+ _SCRIBE_CONFIG="{}"
16
+
17
+ scribe_config_load() {
18
+ local repo_root="${1:-}"
19
+ local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
20
+ local home_dir="${HOME:-}"
21
+
22
+ local merged="{}"
23
+ local file
24
+
25
+ file="${plugin_root}/config.json"
26
+ if [[ -f "$file" ]]; then
27
+ local defaults
28
+ defaults=$(jq '.' "$file" 2>/dev/null) || defaults="{}"
29
+ merged=$(jq -n --argjson a "$merged" --argjson b "$defaults" '$a * $b' 2>/dev/null) \
30
+ || merged="$defaults"
31
+ fi
32
+
33
+ local repo_settings=""
34
+ [[ -n "$repo_root" ]] && repo_settings="${repo_root}/.claude/settings.json"
35
+
36
+ for file in "${home_dir}/.claude/settings.json" "$repo_settings"; do
37
+ [[ -n "$file" && -f "$file" ]] || continue
38
+ local overlay
39
+ overlay=$(jq '{ scribe: (.scribe // {}) }' "$file" 2>/dev/null) || continue
40
+ [[ -z "$overlay" ]] && continue
41
+ local attempt
42
+ if attempt=$(jq -n --argjson a "$merged" --argjson b "$overlay" '
43
+ def deepmerge($a; $b):
44
+ if ($a|type) == "object" and ($b|type) == "object" then
45
+ reduce (($a|keys) + ($b|keys) | unique)[] as $k
46
+ ({}; .[$k] = deepmerge($a[$k]; $b[$k]))
47
+ elif $b == null then $a
48
+ else $b end;
49
+ deepmerge($a; $b)
50
+ ' 2>/dev/null) && [[ -n "$attempt" ]]; then
51
+ merged="$attempt"
52
+ fi
53
+ done
54
+
55
+ _SCRIBE_CONFIG="$merged"
56
+ }
57
+
58
+ scribe_config_get() {
59
+ local path="$1"
60
+ printf '%s' "$_SCRIBE_CONFIG" | jq -r "${path} // empty" 2>/dev/null
61
+ }
62
+
63
+ scribe_config_get_json() {
64
+ local path="$1"
65
+ printf '%s' "$_SCRIBE_CONFIG" | jq -c "${path}" 2>/dev/null
66
+ }
67
+
68
+ scribe_config_enabled() {
69
+ local v
70
+ v=$(scribe_config_get '.scribe.enabled')
71
+ [[ "$v" == "true" ]]
72
+ }