@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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|