@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,239 @@
1
+ #!/usr/bin/env bash
2
+ # Distillation pipeline for Scribe.
3
+ #
4
+ # Orchestrates the full Stop-time flow:
5
+ # 1. Load session state (captured initial prompt)
6
+ # 2. Count transcript turns — skip if below min_turns
7
+ # 3. Call scribe_extract_intent (Haiku pass)
8
+ # 4. Format output as a readable Markdown document
9
+ # 5. Write to ~/.onlooker/scribe/<project_key>/
10
+ # 6. Optionally mirror to <repo_root>/<project_dir>/
11
+ # 7. Emit scribe.distill.complete
12
+ #
13
+ # Exposes:
14
+ # scribe_distill <session_id> <cwd> <transcript_path>
15
+
16
+ # shellcheck source=./scribe-extract.sh
17
+ # (caller must source scribe-extract.sh before scribe-distill.sh)
18
+
19
+ _scribe_format_document() {
20
+ local intent_json="${1:-}"
21
+ local session_id="${2:-unknown}"
22
+ local project_root="${3:-}"
23
+ local captured_prompt="${4:-}"
24
+ local date_str
25
+ date_str=$(date '+%Y-%m-%d' 2>/dev/null) || date_str="unknown"
26
+ local timestamp
27
+ timestamp=$(date '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || timestamp="unknown"
28
+
29
+ local summary problem decisions_json tradeoffs_json constraints_json out_of_scope_json
30
+ summary=$(printf '%s' "$intent_json" | jq -r '.summary // ""' 2>/dev/null) || summary=""
31
+ problem=$(printf '%s' "$intent_json" | jq -r '.problem // ""' 2>/dev/null) || problem=""
32
+ decisions_json=$(printf '%s' "$intent_json" | jq -c '.decisions // []' 2>/dev/null) || decisions_json="[]"
33
+ tradeoffs_json=$(printf '%s' "$intent_json" | jq -c '.tradeoffs // []' 2>/dev/null) || tradeoffs_json="[]"
34
+ constraints_json=$(printf '%s' "$intent_json" | jq -c '.constraints // []' 2>/dev/null) || constraints_json="[]"
35
+ out_of_scope_json=$(printf '%s' "$intent_json" | jq -c '.out_of_scope // []' 2>/dev/null) || out_of_scope_json="[]"
36
+
37
+ local session_short="${session_id:0:8}"
38
+
39
+ {
40
+ printf '# Session Intent: %s\n\n' "$date_str"
41
+ [[ -n "$summary" ]] && printf '> %s\n\n' "$summary"
42
+
43
+ printf '## Problem\n\n'
44
+ if [[ -n "$problem" ]]; then
45
+ printf '%s\n\n' "$problem"
46
+ else
47
+ printf '*No problem statement extracted.*\n\n'
48
+ fi
49
+
50
+ printf '## Decisions\n\n'
51
+ local decision_count
52
+ decision_count=$(printf '%s' "$decisions_json" | jq 'length' 2>/dev/null) || decision_count=0
53
+ if [[ "$decision_count" -gt 0 ]]; then
54
+ local i
55
+ for ((i = 0; i < decision_count; i++)); do
56
+ local d r alts
57
+ d=$(printf '%s' "$decisions_json" | jq -r ".[$i].decision // \"\"" 2>/dev/null) || d=""
58
+ r=$(printf '%s' "$decisions_json" | jq -r ".[$i].reason // \"\"" 2>/dev/null) || r=""
59
+ alts=$(printf '%s' "$decisions_json" | jq -r ".[$i].alternatives // [] | .[]" 2>/dev/null) || alts=""
60
+ [[ -z "$d" ]] && continue
61
+ printf '- **%s** — %s\n' "$d" "$r"
62
+ if [[ -n "$alts" ]]; then
63
+ printf ' - *Considered:* '
64
+ local first=1
65
+ while IFS= read -r alt; do
66
+ [[ -z "$alt" ]] && continue
67
+ [[ "$first" -eq 0 ]] && printf ', '
68
+ printf '%s' "$alt"
69
+ first=0
70
+ done <<< "$alts"
71
+ printf '\n'
72
+ fi
73
+ done
74
+ printf '\n'
75
+ else
76
+ printf '*None noted.*\n\n'
77
+ fi
78
+
79
+ printf '## Tradeoffs\n\n'
80
+ local tradeoff_count
81
+ tradeoff_count=$(printf '%s' "$tradeoffs_json" | jq 'length' 2>/dev/null) || tradeoff_count=0
82
+ if [[ "$tradeoff_count" -gt 0 ]]; then
83
+ printf '%s' "$tradeoffs_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
84
+ [[ -n "$item" ]] && printf '- %s\n' "$item"
85
+ done
86
+ printf '\n'
87
+ else
88
+ printf '*None noted.*\n\n'
89
+ fi
90
+
91
+ printf '## Constraints\n\n'
92
+ local constraint_count
93
+ constraint_count=$(printf '%s' "$constraints_json" | jq 'length' 2>/dev/null) || constraint_count=0
94
+ if [[ "$constraint_count" -gt 0 ]]; then
95
+ printf '%s' "$constraints_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
96
+ [[ -n "$item" ]] && printf '- %s\n' "$item"
97
+ done
98
+ printf '\n'
99
+ else
100
+ printf '*None noted.*\n\n'
101
+ fi
102
+
103
+ printf '## Out of Scope\n\n'
104
+ local oos_count
105
+ oos_count=$(printf '%s' "$out_of_scope_json" | jq 'length' 2>/dev/null) || oos_count=0
106
+ if [[ "$oos_count" -gt 0 ]]; then
107
+ printf '%s' "$out_of_scope_json" | jq -r '.[]' 2>/dev/null | while IFS= read -r item; do
108
+ [[ -n "$item" ]] && printf '- %s\n' "$item"
109
+ done
110
+ printf '\n'
111
+ else
112
+ printf '*None noted.*\n\n'
113
+ fi
114
+
115
+ if [[ -n "$captured_prompt" ]]; then
116
+ printf '## Initial Prompt\n\n'
117
+ printf '```\n%s\n```\n\n' "$captured_prompt"
118
+ fi
119
+
120
+ printf '---\n'
121
+ printf '*Generated by scribe · session `%s` · %s*\n' "$session_short" "$timestamp"
122
+ [[ -n "$project_root" ]] && printf '*Project: `%s`*\n' "$project_root"
123
+ }
124
+ }
125
+
126
+ scribe_distill() {
127
+ local session_id="${1:-}"
128
+ local cwd="${2:-}"
129
+ local transcript_path="${3:-}"
130
+
131
+ [[ -z "$session_id" ]] && return 1
132
+
133
+ local onlooker_dir="${ONLOOKER_DIR:-${HOME}/.onlooker}"
134
+ local state_file="${onlooker_dir}/scribe/sessions/${session_id}.json"
135
+
136
+ # Load captured prompt from session state (best-effort).
137
+ local captured_prompt=""
138
+ if [[ -f "$state_file" ]]; then
139
+ captured_prompt=$(jq -r '.captured_prompt // ""' "$state_file" 2>/dev/null) || captured_prompt=""
140
+ fi
141
+
142
+ # Transcript is required for extraction.
143
+ if [[ -z "$transcript_path" || ! -f "$transcript_path" ]]; then
144
+ printf 'scribe_distill: no transcript available for session %s\n' "$session_id" >&2
145
+ return 1
146
+ fi
147
+
148
+ # Count turns; skip trivial sessions.
149
+ local min_turns
150
+ min_turns=$(scribe_config_get '.scribe.capture.min_turns') || min_turns="3"
151
+ [[ -z "$min_turns" || "$min_turns" == "null" ]] && min_turns="3"
152
+
153
+ local turn_count
154
+ turn_count=$(scribe_count_turns "$transcript_path")
155
+
156
+ if [[ "$turn_count" -lt "$min_turns" ]]; then
157
+ return 2
158
+ fi
159
+
160
+ # Resolve config.
161
+ local model timeout_s max_tokens temperature transcript_chars_max
162
+ model=$(scribe_config_get '.scribe.evaluator.model')
163
+ [[ -z "$model" || "$model" == "null" ]] && model="claude-haiku-4-5-20251001"
164
+ timeout_s=$(scribe_config_get '.scribe.evaluator.timeout')
165
+ [[ -z "$timeout_s" || "$timeout_s" == "null" ]] && timeout_s="60"
166
+ max_tokens=$(scribe_config_get '.scribe.evaluator.max_tokens')
167
+ [[ -z "$max_tokens" || "$max_tokens" == "null" ]] && max_tokens="2048"
168
+ temperature=$(scribe_config_get '.scribe.evaluator.temperature')
169
+ [[ -z "$temperature" || "$temperature" == "null" ]] && temperature="0.3"
170
+ transcript_chars_max=$(scribe_config_get '.scribe.capture.transcript_chars_max')
171
+ [[ -z "$transcript_chars_max" || "$transcript_chars_max" == "null" ]] && transcript_chars_max="40000"
172
+
173
+ # Run extraction.
174
+ local intent_json
175
+ intent_json=$(scribe_extract_intent \
176
+ "$transcript_path" "$model" "$timeout_s" "$max_tokens" "$temperature" "$transcript_chars_max") || {
177
+ printf 'scribe_distill: extraction failed for session %s\n' "$session_id" >&2
178
+ return 1
179
+ }
180
+
181
+ # Resolve project key and output paths.
182
+ local project_key project_root output_dir
183
+ project_key=$(scribe_project_key "$cwd")
184
+ project_root=$(scribe_project_repo_root "$cwd")
185
+
186
+ if [[ -n "$project_key" ]]; then
187
+ output_dir=$(scribe_project_dir "$project_key")
188
+ else
189
+ output_dir="${onlooker_dir}/scribe/unknown"
190
+ fi
191
+
192
+ mkdir -p "$output_dir" 2>/dev/null || {
193
+ printf 'scribe_distill: cannot create output dir %s\n' "$output_dir" >&2
194
+ return 1
195
+ }
196
+
197
+ local date_str
198
+ date_str=$(date '+%Y-%m-%d' 2>/dev/null) || date_str="unknown"
199
+ local session_short="${session_id:0:8}"
200
+ local filename="${date_str}-${session_short}.md"
201
+ local output_path="${output_dir}/${filename}"
202
+
203
+ # Format and write the document.
204
+ local doc
205
+ doc=$(_scribe_format_document \
206
+ "$intent_json" "$session_id" "$project_root" "$captured_prompt")
207
+
208
+ printf '%s\n' "$doc" > "$output_path" 2>/dev/null || {
209
+ printf 'scribe_distill: failed to write %s\n' "$output_path" >&2
210
+ return 1
211
+ }
212
+
213
+ # Mirror to project tree if configured.
214
+ local mirror artifacts=1
215
+ mirror=$(scribe_config_get '.scribe.output.mirror_to_project')
216
+ if [[ "$mirror" == "true" && -n "$project_root" ]]; then
217
+ local project_dir
218
+ project_dir=$(scribe_config_get '.scribe.output.project_dir')
219
+ [[ -z "$project_dir" || "$project_dir" == "null" ]] && project_dir="docs/decisions"
220
+ local mirror_dir="${project_root}/${project_dir}"
221
+ if mkdir -p "$mirror_dir" 2>/dev/null; then
222
+ if cp "$output_path" "${mirror_dir}/${filename}" 2>/dev/null; then
223
+ artifacts=2
224
+ fi
225
+ fi
226
+ fi
227
+
228
+ # Emit scribe.distill.complete.
229
+ local payload
230
+ payload=$(jq -n \
231
+ --arg sid "$session_id" \
232
+ --argjson cap 1 \
233
+ --argjson art "$artifacts" \
234
+ '{session_id: $sid, captures_processed: $cap, artifacts_produced: $art}') || payload=""
235
+
236
+ [[ -n "$payload" ]] && scribe_emit_event "scribe.distill.complete" "$payload" || true
237
+
238
+ printf '%s' "$output_path"
239
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # Canonical scribe.* 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 before being
6
+ # appended to $ONLOOKER_EVENTS_LOG (defaults to $ONLOOKER_DIR/logs/onlooker-events.jsonl).
7
+ #
8
+ # Usage:
9
+ # scribe_emit_event "scribe.distill.complete" '{"session_id":"...","captures_processed":1,...}'
10
+
11
+ _SCRIBE_PLUGIN_NAME="scribe"
12
+
13
+ _scribe_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
+ _scribe_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
+ scribe_emit_event() {
43
+ local event_type="${1:-}"
44
+ local payload="${2:-}"
45
+
46
+ [[ -z "$event_type" || -z "$payload" ]] && return 1
47
+
48
+ local event_js
49
+ event_js=$(_scribe_event_js_path) || return 1
50
+
51
+ local session_id
52
+ session_id=$(_scribe_session_id)
53
+
54
+ local params
55
+ params=$(jq -n \
56
+ --arg plugin "$_SCRIBE_PLUGIN_NAME" \
57
+ --arg sid "$session_id" \
58
+ --arg type "$event_type" \
59
+ --argjson payload "$payload" \
60
+ '{plugin: $plugin, session_id: $sid, event_type: $type, payload: $payload}' \
61
+ 2>/dev/null) || return 1
62
+
63
+ local event
64
+ local stderr_file
65
+ stderr_file=$(mktemp -t scribe-event-err.XXXXXX 2>/dev/null) || stderr_file="/tmp/scribe-event-err.$$"
66
+ event=$(printf '%s' "$params" \
67
+ | ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}" \
68
+ ONLOOKER_PLUGIN_NAME="$_SCRIBE_PLUGIN_NAME" \
69
+ node "$event_js" emit 2>"$stderr_file") || {
70
+ printf 'scribe_emit_event: schema validation failed for %s\n' "$event_type" >&2
71
+ [[ -s "$stderr_file" ]] && cat "$stderr_file" >&2
72
+ rm -f "$stderr_file"
73
+ return 1
74
+ }
75
+ rm -f "$stderr_file"
76
+
77
+ local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
78
+ mkdir -p "$(dirname "$log_path")" 2>/dev/null || return 1
79
+ printf '%s\n' "$event" >> "$log_path"
80
+ }
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # Intent extraction for Scribe.
3
+ #
4
+ # Reads a session transcript and runs a Haiku pass to extract structured
5
+ # intent documentation: the problem being solved, decisions made and why,
6
+ # tradeoffs, constraints, and what was explicitly left out.
7
+ #
8
+ # This is documentation from intent, not from code. The output answers
9
+ # WHY, not WHAT — git logs and code comments cover what.
10
+ #
11
+ # Exposes:
12
+ # scribe_count_turns <transcript_path>
13
+ # Echoes the number of user turns found in the transcript (integer).
14
+ #
15
+ # scribe_extract_intent <transcript_path> <model> <timeout> <max_tokens> <temperature>
16
+ # Echoes a JSON object on success, empty string on failure.
17
+ # JSON shape:
18
+ # {
19
+ # "problem": string,
20
+ # "decisions": [{decision, reason, alternatives:[]}],
21
+ # "tradeoffs": [string],
22
+ # "constraints": [string],
23
+ # "out_of_scope": [string],
24
+ # "summary": string
25
+ # }
26
+
27
+ _SCRIBE_EXTRACT_PROMPT='You are an intent documentation assistant. Analyze this agent session transcript and extract structured documentation about WHY changes were made — the problem context, decisions, tradeoffs, and constraints that shaped the work. This is documentation from intent, not from code.
28
+
29
+ Do NOT describe what was done. Focus exclusively on why decisions were made.
30
+
31
+ Return a JSON object with exactly these keys:
32
+ {
33
+ "problem": "1-3 sentences: what problem or goal initiated this session",
34
+ "decisions": [
35
+ {
36
+ "decision": "what was decided",
37
+ "reason": "why this approach was chosen",
38
+ "alternatives": ["alternative that was considered but rejected"]
39
+ }
40
+ ],
41
+ "tradeoffs": ["tradeoff description — what was gained vs. given up"],
42
+ "constraints": ["constraint that shaped decisions"],
43
+ "out_of_scope": ["what was explicitly not done, and why"],
44
+ "summary": "2-3 sentences: executive summary of the session intent and key decisions"
45
+ }
46
+
47
+ Rules:
48
+ - All fields are required; use empty arrays [] if no items found
49
+ - Keep each item to 1-2 sentences
50
+ - Return ONLY the JSON object — no prose, no markdown fences, no explanation
51
+
52
+ '
53
+
54
+ scribe_count_turns() {
55
+ local transcript_path="${1:-}"
56
+ [[ -f "$transcript_path" ]] || { printf '0'; return 0; }
57
+
58
+ local count=0
59
+ local line
60
+ while IFS= read -r line; do
61
+ [[ -z "$line" ]] && continue
62
+ local role
63
+ role=$(printf '%s' "$line" | jq -r '.role // empty' 2>/dev/null) || continue
64
+ [[ "$role" == "user" ]] && count=$((count + 1))
65
+ done < "$transcript_path"
66
+
67
+ printf '%s' "$count"
68
+ }
69
+
70
+ scribe_extract_intent() {
71
+ local transcript_path="${1:-}"
72
+ local model="${2:-claude-haiku-4-5-20251001}"
73
+ local timeout_s="${3:-60}"
74
+ local max_tokens="${4:-2048}"
75
+ local temperature="${5:-0.3}"
76
+ local transcript_chars_max="${6:-40000}"
77
+
78
+ [[ -f "$transcript_path" ]] || return 1
79
+
80
+ local transcript_content
81
+ transcript_content=$(jq -r '
82
+ select(.role != null) |
83
+ if .role == "user" then
84
+ "[User]\n" + (
85
+ if (.content | type) == "array" then
86
+ [.content[] | select(.type == "text") | .text] | join("\n")
87
+ else
88
+ (.content // "")
89
+ end
90
+ )
91
+ elif .role == "assistant" then
92
+ "[Assistant]\n" + (
93
+ if (.content | type) == "array" then
94
+ [.content[] | select(.type == "text") | .text] | join("\n")
95
+ else
96
+ (.content // "")
97
+ end
98
+ )
99
+ else empty end
100
+ ' "$transcript_path" 2>/dev/null | head -c "$transcript_chars_max") || transcript_content=""
101
+
102
+ [[ -z "$transcript_content" ]] && return 1
103
+
104
+ local prompt_file
105
+ prompt_file=$(mktemp -t scribe-extract.XXXXXX 2>/dev/null) || prompt_file="/tmp/scribe-extract.$$"
106
+ trap 'rm -f "$prompt_file"' RETURN
107
+
108
+ {
109
+ printf '%s' "$_SCRIBE_EXTRACT_PROMPT"
110
+ printf '<session_transcript>\n'
111
+ printf '%s\n' "$transcript_content"
112
+ printf '</session_transcript>\n'
113
+ } > "$prompt_file"
114
+
115
+ if ! command -v claude >/dev/null 2>&1; then
116
+ printf 'scribe_extract_intent: claude CLI not found\n' >&2
117
+ return 1
118
+ fi
119
+
120
+ local claude_args=(-p --max-turns 1 --model "$model" --max-tokens "$max_tokens")
121
+
122
+ local response=""
123
+ if command -v timeout >/dev/null 2>&1; then
124
+ response=$(timeout "$timeout_s" claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
125
+ elif command -v gtimeout >/dev/null 2>&1; then
126
+ response=$(gtimeout "$timeout_s" claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
127
+ else
128
+ response=$(claude "${claude_args[@]}" < "$prompt_file" 2>/dev/null) || response=""
129
+ fi
130
+
131
+ [[ -z "$response" ]] && return 1
132
+
133
+ # Strip markdown fences if present.
134
+ local clean
135
+ clean=$(printf '%s' "$response" \
136
+ | sed -e 's/^```json[[:space:]]*//' -e 's/^```[[:space:]]*//' -e 's/[[:space:]]*```$//')
137
+
138
+ # Validate all required keys from the extraction prompt.
139
+ if ! printf '%s' "$clean" | jq -e \
140
+ '.problem and (.decisions | type == "array") and (.tradeoffs | type == "array") and (.constraints | type == "array") and (.out_of_scope | type == "array") and .summary' \
141
+ >/dev/null 2>&1; then
142
+ printf 'scribe_extract_intent: response missing required keys\n' >&2
143
+ return 1
144
+ fi
145
+
146
+ printf '%s' "$clean"
147
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # Project key derivation for Scribe.
3
+ #
4
+ # Mirrors the tribunal project-key scheme so plugins partition storage
5
+ # identically. A project key is a stable 12-char hex identifier that survives:
6
+ # - local rename of the repo directory
7
+ # - cloning the same repo to a different path on the same machine
8
+ # - moving the repo between machines (as long as the git remote is preserved)
9
+ # - worktrees (a worktree shares its parent repo's key)
10
+ #
11
+ # Resolution order:
12
+ # 1. SHA256(`git remote get-url origin`) — preferred, machine-portable
13
+ # 2. SHA256(realpath of `git rev-parse --show-toplevel`) — fallback for repos
14
+ # without an origin remote
15
+ #
16
+ # Returns the first 12 hex chars. Returns empty string if neither path works.
17
+
18
+ _scribe_sha256_first12() {
19
+ local input="$1"
20
+ if command -v shasum >/dev/null 2>&1; then
21
+ printf '%s' "$input" | shasum -a 256 2>/dev/null | cut -c1-12
22
+ elif command -v sha256sum >/dev/null 2>&1; then
23
+ printf '%s' "$input" | sha256sum 2>/dev/null | cut -c1-12
24
+ else
25
+ return 1
26
+ fi
27
+ }
28
+
29
+ scribe_project_remote_url() {
30
+ local cwd="${1:-}"
31
+ [[ -z "$cwd" || ! -d "$cwd" ]] && return 0
32
+ git -C "$cwd" remote get-url origin 2>/dev/null || true
33
+ }
34
+
35
+ # Worktree-aware: uses common-dir so worktrees share a key with the main repo.
36
+ scribe_project_repo_root() {
37
+ local cwd="${1:-}"
38
+ [[ -z "$cwd" || ! -d "$cwd" ]] && return 0
39
+
40
+ if ! git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
41
+ return 0
42
+ fi
43
+
44
+ local common_dir toplevel
45
+ common_dir=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || return 0
46
+
47
+ if [[ -n "$common_dir" && "$common_dir" != /* ]]; then
48
+ common_dir="$(cd "$cwd" && cd "$common_dir" 2>/dev/null && pwd -P)" || common_dir=""
49
+ fi
50
+
51
+ if [[ -n "$common_dir" && -d "$common_dir" ]]; then
52
+ toplevel="$(cd "$common_dir/.." 2>/dev/null && pwd -P)" || toplevel=""
53
+ fi
54
+
55
+ if [[ -z "$toplevel" ]]; then
56
+ toplevel=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true)
57
+ [[ -n "$toplevel" ]] && toplevel="$(cd "$toplevel" 2>/dev/null && pwd -P)"
58
+ fi
59
+
60
+ printf '%s' "$toplevel"
61
+ }
62
+
63
+ scribe_project_key() {
64
+ local cwd="${1:-}"
65
+ [[ -z "$cwd" ]] && cwd="$(pwd)"
66
+
67
+ local remote
68
+ remote=$(scribe_project_remote_url "$cwd")
69
+ if [[ -n "$remote" ]]; then
70
+ _scribe_sha256_first12 "remote:$remote"
71
+ return 0
72
+ fi
73
+
74
+ local root
75
+ root=$(scribe_project_repo_root "$cwd")
76
+ if [[ -n "$root" ]]; then
77
+ _scribe_sha256_first12 "root:$root"
78
+ return 0
79
+ fi
80
+
81
+ return 0
82
+ }
83
+
84
+ scribe_project_dir() {
85
+ local project_key="${1:-}"
86
+ [[ -z "$project_key" ]] && return 1
87
+ local onlooker_dir="${ONLOOKER_DIR:-${HOME}/.onlooker}"
88
+ printf '%s' "${onlooker_dir}/scribe/${project_key}"
89
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # Minimal ULID generator for Scribe document and event IDs.
3
+ #
4
+ # Spec: https://github.com/ulid/spec
5
+ # - 48-bit timestamp (ms since epoch) → 10 chars Crockford Base32
6
+ # - 80-bit randomness → 16 chars Crockford Base32
7
+ # - lexicographically sortable, time-ordered
8
+
9
+ _SCRIBE_ULID_ALPHABET="0123456789ABCDEFGHJKMNPQRSTVWXYZ"
10
+
11
+ _scribe_ulid_encode() {
12
+ local n="$1"
13
+ local len="$2"
14
+ local out=""
15
+ local i
16
+ for ((i = 0; i < len; i++)); do
17
+ out="${_SCRIBE_ULID_ALPHABET:$((n % 32)):1}${out}"
18
+ n=$((n / 32))
19
+ done
20
+ printf '%s' "$out"
21
+ }
22
+
23
+ scribe_ulid() {
24
+ local now_ms
25
+ if [[ "$(uname)" == "Darwin" ]]; then
26
+ now_ms=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null) \
27
+ || now_ms=$(($(date +%s) * 1000))
28
+ else
29
+ now_ms=$(date +%s%3N 2>/dev/null) || now_ms=$(($(date +%s) * 1000))
30
+ fi
31
+
32
+ local rand_hex rand_hi rand_lo
33
+ rand_hex=$(openssl rand -hex 10 2>/dev/null)
34
+ if [[ -n "$rand_hex" && ${#rand_hex} -eq 20 ]]; then
35
+ rand_hi=$((16#${rand_hex:0:10}))
36
+ rand_lo=$((16#${rand_hex:10:10}))
37
+ else
38
+ rand_hi=$((RANDOM * 32768 + RANDOM))
39
+ rand_lo=$((RANDOM * 32768 + RANDOM))
40
+ rand_hi=$(((rand_hi * 256 + RANDOM % 256) & ((1 << 40) - 1)))
41
+ rand_lo=$(((rand_lo * 256 + RANDOM % 256) & ((1 << 40) - 1)))
42
+ fi
43
+
44
+ local ts_part hi_part lo_part
45
+ ts_part=$(_scribe_ulid_encode "$now_ms" 10)
46
+ hi_part=$(_scribe_ulid_encode "$rand_hi" 8)
47
+ lo_part=$(_scribe_ulid_encode "$rand_lo" 8)
48
+
49
+ printf '%s%s%s' "$ts_part" "$hi_part" "$lo_part"
50
+ }
@@ -94,6 +94,54 @@
94
94
  "jsonpath": "$.version"
95
95
  }
96
96
  ]
97
+ },
98
+ "plugins/compass": {
99
+ "changelog-path": "CHANGELOG.md",
100
+ "release-type": "simple",
101
+ "bump-minor-pre-major": true,
102
+ "bump-patch-for-minor-pre-major": false,
103
+ "component": "compass",
104
+ "draft": false,
105
+ "prerelease": false,
106
+ "extra-files": [
107
+ {
108
+ "type": "json",
109
+ "path": ".claude-plugin/plugin.json",
110
+ "jsonpath": "$.version"
111
+ }
112
+ ]
113
+ },
114
+ "plugins/scribe": {
115
+ "changelog-path": "CHANGELOG.md",
116
+ "release-type": "simple",
117
+ "bump-minor-pre-major": true,
118
+ "bump-patch-for-minor-pre-major": false,
119
+ "component": "scribe",
120
+ "draft": false,
121
+ "prerelease": false,
122
+ "extra-files": [
123
+ {
124
+ "type": "json",
125
+ "path": ".claude-plugin/plugin.json",
126
+ "jsonpath": "$.version"
127
+ }
128
+ ]
129
+ },
130
+ "plugins/counsel": {
131
+ "changelog-path": "CHANGELOG.md",
132
+ "release-type": "simple",
133
+ "bump-minor-pre-major": true,
134
+ "bump-patch-for-minor-pre-major": false,
135
+ "component": "counsel",
136
+ "draft": false,
137
+ "prerelease": false,
138
+ "extra-files": [
139
+ {
140
+ "type": "json",
141
+ "path": ".claude-plugin/plugin.json",
142
+ "jsonpath": "$.version"
143
+ }
144
+ ]
97
145
  }
98
146
  },
99
147
  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"