@onlooker-community/ecosystem 0.16.0 → 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 +26 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +4 -2
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +88 -0
- package/package.json +2 -2
- 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 +1 -1
- package/plugins/governor/CHANGELOG.md +7 -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 +32 -0
- package/test/bats/scribe-extract.bats +102 -0
- package/test/bats/scribe-project-key.bats +75 -0
|
@@ -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,38 @@
|
|
|
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
|
+
]
|
|
97
129
|
}
|
|
98
130
|
},
|
|
99
131
|
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
setup_test_env
|
|
7
|
+
|
|
8
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/scribe"
|
|
9
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-config.sh"
|
|
12
|
+
# shellcheck disable=SC1091
|
|
13
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-extract.sh"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# scribe_count_turns
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
@test "count_turns returns 0 for missing file" {
|
|
21
|
+
run scribe_count_turns "/nonexistent/path.jsonl"
|
|
22
|
+
[ "$status" -eq 0 ]
|
|
23
|
+
[ "$output" = "0" ]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@test "count_turns returns 0 for empty file" {
|
|
27
|
+
local f="${BATS_TEST_TMPDIR}/empty.jsonl"
|
|
28
|
+
touch "$f"
|
|
29
|
+
run scribe_count_turns "$f"
|
|
30
|
+
[ "$status" -eq 0 ]
|
|
31
|
+
[ "$output" = "0" ]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@test "count_turns counts only user-role entries" {
|
|
35
|
+
local f="${BATS_TEST_TMPDIR}/transcript.jsonl"
|
|
36
|
+
printf '%s\n' \
|
|
37
|
+
'{"role":"user","content":"hello"}' \
|
|
38
|
+
'{"role":"assistant","content":"hi"}' \
|
|
39
|
+
'{"role":"user","content":"thanks"}' \
|
|
40
|
+
'{"role":"assistant","content":"sure"}' \
|
|
41
|
+
> "$f"
|
|
42
|
+
run scribe_count_turns "$f"
|
|
43
|
+
[ "$status" -eq 0 ]
|
|
44
|
+
[ "$output" = "2" ]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@test "count_turns skips blank lines" {
|
|
48
|
+
local f="${BATS_TEST_TMPDIR}/sparse.jsonl"
|
|
49
|
+
printf '%s\n' \
|
|
50
|
+
'{"role":"user","content":"a"}' \
|
|
51
|
+
'' \
|
|
52
|
+
'{"role":"user","content":"b"}' \
|
|
53
|
+
> "$f"
|
|
54
|
+
run scribe_count_turns "$f"
|
|
55
|
+
[ "$status" -eq 0 ]
|
|
56
|
+
[ "$output" = "2" ]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# min_turns gate (via scribe_distill return code)
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
@test "scribe_distill exits 2 when transcript has fewer turns than min_turns" {
|
|
64
|
+
# shellcheck disable=SC1091
|
|
65
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-events.sh"
|
|
66
|
+
# shellcheck disable=SC1091
|
|
67
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-project-key.sh"
|
|
68
|
+
# shellcheck disable=SC1091
|
|
69
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-distill.sh"
|
|
70
|
+
|
|
71
|
+
# Load config so min_turns is available (default: 3).
|
|
72
|
+
scribe_config_load ""
|
|
73
|
+
|
|
74
|
+
# Transcript with only 1 user turn — below default min_turns of 3.
|
|
75
|
+
local f="${BATS_TEST_TMPDIR}/short.jsonl"
|
|
76
|
+
printf '%s\n' \
|
|
77
|
+
'{"role":"user","content":"hey"}' \
|
|
78
|
+
'{"role":"assistant","content":"yo"}' \
|
|
79
|
+
> "$f"
|
|
80
|
+
|
|
81
|
+
run scribe_distill "test-session-id" "${BATS_TEST_TMPDIR}" "$f"
|
|
82
|
+
[ "$status" -eq 2 ]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@test "scribe_distill min_turns skip produces no stderr output" {
|
|
86
|
+
# shellcheck disable=SC1091
|
|
87
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-events.sh"
|
|
88
|
+
# shellcheck disable=SC1091
|
|
89
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-project-key.sh"
|
|
90
|
+
# shellcheck disable=SC1091
|
|
91
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-distill.sh"
|
|
92
|
+
|
|
93
|
+
scribe_config_load ""
|
|
94
|
+
|
|
95
|
+
local f="${BATS_TEST_TMPDIR}/short2.jsonl"
|
|
96
|
+
printf '%s\n' '{"role":"user","content":"hi"}' > "$f"
|
|
97
|
+
|
|
98
|
+
# Run in a subshell capturing stderr separately.
|
|
99
|
+
local stderr_out
|
|
100
|
+
stderr_out=$(scribe_distill "test-sid" "${BATS_TEST_TMPDIR}" "$f" 2>&1 >/dev/null) || true
|
|
101
|
+
[ -z "$stderr_out" ]
|
|
102
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
setup_test_env
|
|
7
|
+
|
|
8
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/scribe"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/scribe-project-key.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "non-git directory returns empty key" {
|
|
14
|
+
local d="${BATS_TEST_TMPDIR}/non-git"
|
|
15
|
+
mkdir -p "$d"
|
|
16
|
+
run scribe_project_key "$d"
|
|
17
|
+
[ "$status" -eq 0 ]
|
|
18
|
+
[ -z "$output" ]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@test "git repo without remote falls back to repo-root hash" {
|
|
22
|
+
local d="${BATS_TEST_TMPDIR}/local-only-repo"
|
|
23
|
+
mkdir -p "$d"
|
|
24
|
+
git -C "$d" init -q
|
|
25
|
+
git -C "$d" config user.email t@example.com
|
|
26
|
+
git -C "$d" config user.name "Test"
|
|
27
|
+
|
|
28
|
+
local k1
|
|
29
|
+
k1=$(scribe_project_key "$d")
|
|
30
|
+
[ -n "$k1" ]
|
|
31
|
+
[ "${#k1}" -eq 12 ]
|
|
32
|
+
|
|
33
|
+
# Stability: a second call returns the same key.
|
|
34
|
+
local k2
|
|
35
|
+
k2=$(scribe_project_key "$d")
|
|
36
|
+
[ "$k1" = "$k2" ]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test "git repo with remote uses remote hash, ignores local path" {
|
|
40
|
+
local a="${BATS_TEST_TMPDIR}/clone-a"
|
|
41
|
+
local b="${BATS_TEST_TMPDIR}/clone-b"
|
|
42
|
+
mkdir -p "$a" "$b"
|
|
43
|
+
for d in "$a" "$b"; do
|
|
44
|
+
git -C "$d" init -q
|
|
45
|
+
git -C "$d" config user.email t@example.com
|
|
46
|
+
git -C "$d" config user.name "Test"
|
|
47
|
+
git -C "$d" remote add origin git@github.com:org/proj.git
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
local ka kb
|
|
51
|
+
ka=$(scribe_project_key "$a")
|
|
52
|
+
kb=$(scribe_project_key "$b")
|
|
53
|
+
[ -n "$ka" ]
|
|
54
|
+
[ "$ka" = "$kb" ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "different remotes yield different keys" {
|
|
58
|
+
local a="${BATS_TEST_TMPDIR}/proj-a"
|
|
59
|
+
local b="${BATS_TEST_TMPDIR}/proj-b"
|
|
60
|
+
mkdir -p "$a" "$b"
|
|
61
|
+
for d in "$a" "$b"; do
|
|
62
|
+
git -C "$d" init -q
|
|
63
|
+
git -C "$d" config user.email t@example.com
|
|
64
|
+
git -C "$d" config user.name "Test"
|
|
65
|
+
done
|
|
66
|
+
git -C "$a" remote add origin git@github.com:org/proj-a.git
|
|
67
|
+
git -C "$b" remote add origin git@github.com:org/proj-b.git
|
|
68
|
+
|
|
69
|
+
local ka kb
|
|
70
|
+
ka=$(scribe_project_key "$a")
|
|
71
|
+
kb=$(scribe_project_key "$b")
|
|
72
|
+
[ -n "$ka" ]
|
|
73
|
+
[ -n "$kb" ]
|
|
74
|
+
[ "$ka" != "$kb" ]
|
|
75
|
+
}
|