@onlooker-community/ecosystem 0.32.1 → 0.33.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/plugin.json +1 -1
- package/.release-please-manifest.json +4 -4
- package/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/plugins/bursar/.claude-plugin/plugin.json +1 -1
- package/plugins/bursar/CHANGELOG.md +12 -0
- package/plugins/bursar/scripts/hooks/bursar-session-end.sh +6 -5
- package/plugins/bursar/scripts/lib/bursar-config.sh +10 -43
- package/plugins/governor/.claude-plugin/plugin.json +1 -1
- package/plugins/governor/CHANGELOG.md +7 -0
- package/plugins/governor/scripts/hooks/governor-post-tool-use.sh +9 -6
- package/plugins/governor/scripts/lib/governor-ledger.sh +6 -8
- package/plugins/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +7 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +38 -3
- package/scripts/lib/config-loader.sh +131 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Observability substrate for Claude Code. Provides the shared $ONLOOKER_DIR storage root (default $HOME/.onlooker), canonical schema-validated event emission, session and tool tracking hooks, and prompt rules. Required by all other Onlooker plugins.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
".": "0.
|
|
2
|
+
".": "0.33.0",
|
|
3
3
|
"plugins/archivist": "0.3.1",
|
|
4
4
|
"plugins/tribunal": "1.1.1",
|
|
5
5
|
"plugins/echo": "0.3.1",
|
|
6
6
|
"plugins/cartographer": "0.3.1",
|
|
7
|
-
"plugins/governor": "0.3.
|
|
7
|
+
"plugins/governor": "0.3.2",
|
|
8
8
|
"plugins/compass": "0.4.1",
|
|
9
9
|
"plugins/scribe": "0.4.1",
|
|
10
10
|
"plugins/counsel": "0.5.1",
|
|
11
11
|
"plugins/warden": "0.3.1",
|
|
12
|
-
"plugins/librarian": "0.6.
|
|
12
|
+
"plugins/librarian": "0.6.1",
|
|
13
13
|
"plugins/curator": "0.2.1",
|
|
14
14
|
"plugins/historian": "0.3.2",
|
|
15
15
|
"plugins/assayer": "1.1.1",
|
|
16
|
-
"plugins/bursar": "0.
|
|
16
|
+
"plugins/bursar": "0.3.0",
|
|
17
17
|
"plugins/lineage": "0.2.1",
|
|
18
18
|
"plugins/inspector": "0.3.1"
|
|
19
19
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.33.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.32.2...ecosystem-v0.33.0) (2026-08-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **config:** add shared config loader supporting all five settings layers :sparkles: ([#123](https://github.com/onlooker-community/ecosystem/issues/123)) ([c048b76](https://github.com/onlooker-community/ecosystem/commit/c048b76bee2b39abeb7f1af77181191dd48f9cf5))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **hooks:** prevent SessionEnd timeouts causing breadcrumb accumulation ([#121](https://github.com/onlooker-community/ecosystem/issues/121)) ([3c7a409](https://github.com/onlooker-community/ecosystem/commit/3c7a40937aa3ea8c1db85dd0bb3fcc22589bbb49))
|
|
14
|
+
|
|
15
|
+
## [0.32.2](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.32.1...ecosystem-v0.32.2) (2026-08-02)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* librarian + bursar + governor improvements ([#119](https://github.com/onlooker-community/ecosystem/issues/119)) ([d7cdab5](https://github.com/onlooker-community/ecosystem/commit/d7cdab528ea5181ea35da68fbc5ad2e5b064abea))
|
|
21
|
+
|
|
3
22
|
## [0.32.1](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.32.0...ecosystem-v0.32.1) (2026-08-02)
|
|
4
23
|
|
|
5
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bursar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Multi-session, per-project budget accounting for the Onlooker ecosystem. Rolls each session's spend into a per-project ledger on SessionEnd and surfaces \"this project burned $X this week\" at SessionStart. Where governor regulates a single session, bursar is the cross-session rollup: it reads governor.session.complete off the shared event bus and emits bursar.* events for audit. Named for the officer who keeps the accounts. Builds on the Onlooker ecosystem plugin.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/onlooker-community/ecosystem/compare/bursar-v0.2.2...bursar-v0.3.0) (2026-08-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **config:** add shared config loader supporting all five settings layers :sparkles: ([#123](https://github.com/onlooker-community/ecosystem/issues/123)) ([c048b76](https://github.com/onlooker-community/ecosystem/commit/c048b76bee2b39abeb7f1af77181191dd48f9cf5))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **hooks:** prevent SessionEnd timeouts causing breadcrumb accumulation ([#121](https://github.com/onlooker-community/ecosystem/issues/121)) ([3c7a409](https://github.com/onlooker-community/ecosystem/commit/3c7a40937aa3ea8c1db85dd0bb3fcc22589bbb49))
|
|
14
|
+
|
|
3
15
|
## [0.2.2](https://github.com/onlooker-community/ecosystem/compare/bursar-v0.2.1...bursar-v0.2.2) (2026-08-02)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -148,12 +148,13 @@ MODEL=""
|
|
|
148
148
|
# must keep the breadcrumb so the session→project attribution survives for a
|
|
149
149
|
# later attempt rather than being lost behind a false "recorded" event.
|
|
150
150
|
#
|
|
151
|
-
# Use a
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
151
|
+
# Use a short lock timeout (1s) to ensure the hook completes within the CLI's
|
|
152
|
+
# 1.5s SessionEnd budget. If the lock is contended, fail fast and retain the
|
|
153
|
+
# breadcrumb for retry on a future session. Retries on subsequent SessionStart
|
|
154
|
+
# will eventually succeed when contention subsides; this prevents the hook from
|
|
155
|
+
# being cancelled mid-execution by the CLI timeout.
|
|
155
156
|
if [[ -n "$RECORD" ]]; then
|
|
156
|
-
if bursar_ledger_record "$PROJECT_KEY" "$RECORD"
|
|
157
|
+
if bursar_ledger_record "$PROJECT_KEY" "$RECORD" 1; then
|
|
157
158
|
[[ -n "$EV" ]] && bursar_emit_event "bursar.session.recorded" "$EV" "$SESSION_ID" || true
|
|
158
159
|
rm -f "$BREADCRUMB" 2>/dev/null || true
|
|
159
160
|
else
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Config resolution for bursar.
|
|
3
3
|
#
|
|
4
|
-
# Reads
|
|
4
|
+
# Uses the shared config loader from ecosystem. Reads five layers, latest wins:
|
|
5
5
|
# 1. plugins/bursar/config.json (defaults shipped with the plugin)
|
|
6
6
|
# 2. ~/.claude/settings.json
|
|
7
|
-
# 3.
|
|
7
|
+
# 3. ~/.claude/settings.local.json (local overrides user)
|
|
8
|
+
# 4. <repo>/.claude/settings.json
|
|
9
|
+
# 5. <repo>/.claude/settings.local.json (local overrides project)
|
|
8
10
|
#
|
|
9
11
|
# Exposes:
|
|
10
12
|
# bursar_config_load <repo_root> # populates _BURSAR_CONFIG (JSON)
|
|
@@ -14,60 +16,25 @@
|
|
|
14
16
|
# bursar_config_surface_enabled # 0 if bursar.surface_at_session_start is true
|
|
15
17
|
# bursar_config_week_start # echoes "monday" or "sunday"
|
|
16
18
|
|
|
19
|
+
# shellcheck source=../../../scripts/lib/config-loader.sh
|
|
20
|
+
source "${PLUGIN_ROOT}/../../scripts/lib/config-loader.sh"
|
|
21
|
+
|
|
17
22
|
_BURSAR_CONFIG="{}"
|
|
18
23
|
|
|
19
24
|
bursar_config_load() {
|
|
20
25
|
local repo_root="${1:-}"
|
|
21
|
-
|
|
22
|
-
local home_dir="${HOME:-}"
|
|
23
|
-
|
|
24
|
-
# Read each layer's raw text with the no-fork `$(<file)` builtin (NOT `cat`),
|
|
25
|
-
# then deep-merge all three layers in a SINGLE jq invocation. The dominant
|
|
26
|
-
# cost in the SessionEnd hook is jq process startup, not the merge itself, so
|
|
27
|
-
# this collapses what was one-jq-per-file (up to 6 forks) down to one.
|
|
28
|
-
local default_txt="" home_txt="" repo_txt=""
|
|
29
|
-
local default_file="${plugin_root}/config.json"
|
|
30
|
-
local home_file="${home_dir}/.claude/settings.json"
|
|
31
|
-
local repo_file=""
|
|
32
|
-
[[ -n "$repo_root" ]] && repo_file="${repo_root}/.claude/settings.json"
|
|
33
|
-
|
|
34
|
-
[[ -f "$default_file" ]] && default_txt="$(<"$default_file")"
|
|
35
|
-
[[ -f "$home_file" ]] && home_txt="$(<"$home_file")"
|
|
36
|
-
[[ -n "$repo_file" && -f "$repo_file" ]] && repo_txt="$(<"$repo_file")"
|
|
37
|
-
|
|
38
|
-
# Precedence (latest wins): defaults < home settings < repo settings. The
|
|
39
|
-
# defaults file is merged whole; settings files contribute only their .bursar
|
|
40
|
-
# key. `fromjson? // {}` parses each layer defensively — a missing or malformed
|
|
41
|
-
# file degrades to {} rather than aborting the merge (matches the prior
|
|
42
|
-
# per-file fallback).
|
|
43
|
-
_BURSAR_CONFIG=$(jq -n \
|
|
44
|
-
--arg d "$default_txt" \
|
|
45
|
-
--arg h "$home_txt" \
|
|
46
|
-
--arg r "$repo_txt" \
|
|
47
|
-
'
|
|
48
|
-
def deepmerge($a; $b):
|
|
49
|
-
if ($a|type) == "object" and ($b|type) == "object" then
|
|
50
|
-
reduce (($a|keys) + ($b|keys) | unique)[] as $k
|
|
51
|
-
({}; .[$k] = deepmerge($a[$k]; $b[$k]))
|
|
52
|
-
elif $b == null then $a
|
|
53
|
-
else $b end;
|
|
54
|
-
($d | fromjson? // {}) as $defaults
|
|
55
|
-
| (($h | fromjson? // {}) | {bursar: (.bursar // {})}) as $home
|
|
56
|
-
| (($r | fromjson? // {}) | {bursar: (.bursar // {})}) as $repo
|
|
57
|
-
| deepmerge(deepmerge($defaults; $home); $repo)
|
|
58
|
-
' 2>/dev/null) || _BURSAR_CONFIG="{}"
|
|
59
|
-
[[ -z "$_BURSAR_CONFIG" ]] && _BURSAR_CONFIG="{}"
|
|
26
|
+
config_load_plugin "bursar" "$repo_root" "_BURSAR_CONFIG"
|
|
60
27
|
return 0
|
|
61
28
|
}
|
|
62
29
|
|
|
63
30
|
bursar_config_get() {
|
|
64
31
|
local path="$1"
|
|
65
|
-
|
|
32
|
+
config_get "_BURSAR_CONFIG" "${path}"
|
|
66
33
|
}
|
|
67
34
|
|
|
68
35
|
bursar_config_get_json() {
|
|
69
36
|
local path="$1"
|
|
70
|
-
|
|
37
|
+
config_get_json "_BURSAR_CONFIG" "${path}"
|
|
71
38
|
}
|
|
72
39
|
|
|
73
40
|
bursar_config_surface_enabled() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "governor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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.3.2](https://github.com/onlooker-community/ecosystem/compare/governor-v0.3.1...governor-v0.3.2) (2026-08-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* librarian + bursar + governor improvements ([#119](https://github.com/onlooker-community/ecosystem/issues/119)) ([d7cdab5](https://github.com/onlooker-community/ecosystem/commit/d7cdab528ea5181ea35da68fbc5ad2e5b064abea))
|
|
9
|
+
|
|
3
10
|
## [0.3.1](https://github.com/onlooker-community/ecosystem/compare/governor-v0.3.0...governor-v0.3.1) (2026-08-01)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -83,19 +83,21 @@ ESTIMATED_COST=$(governor_estimate_cost "$ESTIMATED_TOKENS")
|
|
|
83
83
|
ESTIMATION_METHOD=$(governor_estimate_method)
|
|
84
84
|
|
|
85
85
|
# Build the completion ledger record.
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
86
|
+
# Keep positive estimated_tokens (do not negate) so session totals reflect
|
|
87
|
+
# projected spend. Reservations are recorded with record_type: "reservation";
|
|
88
|
+
# completions are recorded with record_type: "completion". Ledger totals sum
|
|
89
|
+
# only completion records, so each spawn contributes its estimate once.
|
|
90
|
+
# actual_tokens (when present) will eventually replace estimates when API
|
|
91
|
+
# usage metrics become available.
|
|
89
92
|
AGENT_TYPE="${TOOL_NAME:-Task}"
|
|
90
93
|
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null) || TS="1970-01-01T00:00:00Z"
|
|
91
|
-
NEG_ESTIMATED=$(( -ESTIMATED_TOKENS ))
|
|
92
94
|
|
|
93
95
|
RECORD=$(jq -n \
|
|
94
96
|
--arg ts "$TS" \
|
|
95
97
|
--arg sid "$SESSION_ID" \
|
|
96
98
|
--arg aid "${CLAUDE_SESSION_ID:-unknown}" \
|
|
97
99
|
--arg at "$AGENT_TYPE" \
|
|
98
|
-
--argjson est "$
|
|
100
|
+
--argjson est "$ESTIMATED_TOKENS" \
|
|
99
101
|
--argjson cost "$ESTIMATED_COST" \
|
|
100
102
|
--argjson dur "$DURATION_MS" \
|
|
101
103
|
'{
|
|
@@ -105,7 +107,8 @@ RECORD=$(jq -n \
|
|
|
105
107
|
agent_type: $at,
|
|
106
108
|
estimated_tokens: $est,
|
|
107
109
|
cost_usd_estimated: $cost,
|
|
108
|
-
duration_ms: $dur
|
|
110
|
+
duration_ms: $dur,
|
|
111
|
+
record_type: "completion"
|
|
109
112
|
}' 2>/dev/null) || RECORD="{}"
|
|
110
113
|
|
|
111
114
|
# Compute actual total once; used for both the ledger record and the event payload.
|
|
@@ -126,12 +126,10 @@ _governor_ledger_poison() {
|
|
|
126
126
|
|
|
127
127
|
# Running total of tokens for a session.
|
|
128
128
|
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
# Completed Task records: estimated_tokens = -N_est, actual_tokens = N_act → counts N_act
|
|
134
|
-
# Net: in-flight estimates + completed actuals.
|
|
129
|
+
# Sums all ledger records:
|
|
130
|
+
# - reservation records (in-flight projected spend)
|
|
131
|
+
# - completion records (which may cancel reservations and add actual_tokens)
|
|
132
|
+
# - legacy records with no record_type
|
|
135
133
|
#
|
|
136
134
|
# Usage: tokens=$(governor_ledger_total_tokens "$session_id")
|
|
137
135
|
governor_ledger_total_tokens() {
|
|
@@ -145,7 +143,7 @@ governor_ledger_total_tokens() {
|
|
|
145
143
|
"$ledger_path" 2>/dev/null || printf '0'
|
|
146
144
|
}
|
|
147
145
|
|
|
148
|
-
# Running total of cost for a session (same
|
|
146
|
+
# Running total of cost for a session (same logic as tokens: all records).
|
|
149
147
|
# Usage: cost=$(governor_ledger_total_cost "$session_id")
|
|
150
148
|
governor_ledger_total_cost() {
|
|
151
149
|
local session_id="${1:-}"
|
|
@@ -158,7 +156,7 @@ governor_ledger_total_cost() {
|
|
|
158
156
|
"$ledger_path" 2>/dev/null || printf '0'
|
|
159
157
|
}
|
|
160
158
|
|
|
161
|
-
# Count
|
|
159
|
+
# Count Task calls, excluding reservation records.
|
|
162
160
|
# Usage: calls=$(governor_ledger_call_count "$session_id")
|
|
163
161
|
governor_ledger_call_count() {
|
|
164
162
|
local session_id="${1:-}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "librarian",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Consolidation layer between archivist's per-session artifacts and the user's durable typed memory store. Detects which session decisions, dead-ends, and open questions deserve to live across sessions, classifies them into the user/feedback/project/reference types, and queues them as proposals for explicit confirmation. Auto-promotion is opt-in. 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.6.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.6.0...librarian-v0.6.1) (2026-08-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **hooks:** prevent SessionEnd timeouts causing breadcrumb accumulation ([#121](https://github.com/onlooker-community/ecosystem/issues/121)) ([3c7a409](https://github.com/onlooker-community/ecosystem/commit/3c7a40937aa3ea8c1db85dd0bb3fcc22589bbb49))
|
|
9
|
+
|
|
3
10
|
## [0.6.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.5.0...librarian-v0.6.0) (2026-08-02)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -60,6 +60,17 @@ source "${PLUGIN_ROOT}/scripts/lib/librarian-classifier.sh"
|
|
|
60
60
|
# shellcheck source=../lib/librarian-conflict-detector.sh
|
|
61
61
|
source "${PLUGIN_ROOT}/scripts/lib/librarian-conflict-detector.sh"
|
|
62
62
|
|
|
63
|
+
librarian_now_ms() {
|
|
64
|
+
local now_ms
|
|
65
|
+
now_ms=$(python3 - <<'PY' 2>/dev/null
|
|
66
|
+
import time
|
|
67
|
+
print(int(time.time() * 1000))
|
|
68
|
+
PY
|
|
69
|
+
) || now_ms=""
|
|
70
|
+
[[ -z "$now_ms" ]] && now_ms=$(( $(date +%s) * 1000 ))
|
|
71
|
+
printf '%s' "$now_ms"
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
INPUT=$(cat 2>/dev/null || true)
|
|
64
75
|
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
|
|
65
76
|
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
|
|
@@ -98,7 +109,7 @@ fi
|
|
|
98
109
|
# Emit scan.started and load candidate window.
|
|
99
110
|
# ----------------------------------------------------------------------------
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
SCAN_START_TS_MS=$(librarian_now_ms)
|
|
102
113
|
ARTIFACTS=$(librarian_archivist_load_since "$PROJECT_KEY" "$WATERMARK")
|
|
103
114
|
ARTIFACT_COUNT=$(printf '%s' "$ARTIFACTS" | jq 'length' 2>/dev/null) || ARTIFACT_COUNT=0
|
|
104
115
|
|
|
@@ -114,7 +125,7 @@ librarian_emit "librarian.scan.started" "$SESSION_ID" "$(jq -cn \
|
|
|
114
125
|
# don't re-walk the same window.
|
|
115
126
|
if [[ "$ARTIFACT_COUNT" == "0" ]]; then
|
|
116
127
|
librarian_storage_write_last_scan "$PROJECT_KEY" || true
|
|
117
|
-
DURATION_MS=$((
|
|
128
|
+
DURATION_MS=$(( $(librarian_now_ms) - SCAN_START_TS_MS ))
|
|
118
129
|
librarian_emit "librarian.scan.complete" "$SESSION_ID" "$(jq -cn \
|
|
119
130
|
--arg outcome "empty" \
|
|
120
131
|
--argjson duration_ms "$DURATION_MS" \
|
|
@@ -155,6 +166,30 @@ for ((i = 0; i < DROPPED_EMIT_COUNT; i++)); do
|
|
|
155
166
|
| with_entries(select(.value != null))')"
|
|
156
167
|
done
|
|
157
168
|
|
|
169
|
+
# ----------------------------------------------------------------------------
|
|
170
|
+
# Runtime budget check: SessionEnd has 1.5s total. If we're running low on time,
|
|
171
|
+
# skip classification to ensure scan.complete can be emitted before CLI timeout.
|
|
172
|
+
# Retained artifacts will be re-scanned on the next session when time permits.
|
|
173
|
+
# ----------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
ELAPSED_MS=$(( $(librarian_now_ms) - SCAN_START_TS_MS ))
|
|
176
|
+
BUDGET_THRESHOLD_MS=1000
|
|
177
|
+
if [[ "$ELAPSED_MS" -ge "$BUDGET_THRESHOLD_MS" ]]; then
|
|
178
|
+
librarian_storage_write_last_scan "$PROJECT_KEY" || true
|
|
179
|
+
DURATION_MS=$(( $(librarian_now_ms) - SCAN_START_TS_MS ))
|
|
180
|
+
librarian_emit "librarian.scan.complete" "$SESSION_ID" "$(jq -cn \
|
|
181
|
+
--arg outcome "budget_exceeded" \
|
|
182
|
+
--argjson duration_ms "$DURATION_MS" \
|
|
183
|
+
--argjson candidates_proposed 0 \
|
|
184
|
+
--argjson candidates_dropped "$DROPPED_TOTAL" \
|
|
185
|
+
--argjson artifact_count_in_window "$ARTIFACT_COUNT" \
|
|
186
|
+
'{ outcome: $outcome, duration_ms: $duration_ms,
|
|
187
|
+
candidates_proposed: $candidates_proposed,
|
|
188
|
+
candidates_dropped: $candidates_dropped,
|
|
189
|
+
artifact_count_in_window: $artifact_count_in_window }')"
|
|
190
|
+
exit 0
|
|
191
|
+
fi
|
|
192
|
+
|
|
158
193
|
# ----------------------------------------------------------------------------
|
|
159
194
|
# Classifier loop — one Haiku call per surviving candidate.
|
|
160
195
|
# ----------------------------------------------------------------------------
|
|
@@ -398,7 +433,7 @@ librarian_storage_write_last_scan "$PROJECT_KEY" || true
|
|
|
398
433
|
TOTAL_DROPPED=$((DROPPED_TOTAL + POST_CLASSIFIER_DROPPED))
|
|
399
434
|
OUTCOME="ok"
|
|
400
435
|
[[ "$PROPOSED_COUNT" == "0" ]] && OUTCOME="empty"
|
|
401
|
-
DURATION_MS=$((
|
|
436
|
+
DURATION_MS=$(( $(librarian_now_ms) - SCAN_START_TS_MS ))
|
|
402
437
|
|
|
403
438
|
librarian_emit "librarian.scan.complete" "$SESSION_ID" "$(jq -cn \
|
|
404
439
|
--arg outcome "$OUTCOME" \
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared config loader for Onlooker plugins.
|
|
3
|
+
#
|
|
4
|
+
# Plugins use this to avoid duplicating config merging logic. Handles all five
|
|
5
|
+
# layers of settings precedence in a single jq pass, with consistent behavior
|
|
6
|
+
# across the ecosystem.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# # In your plugin's config loader (e.g., plugins/bursar/scripts/lib/bursar-config.sh):
|
|
10
|
+
# source "${PLUGIN_ROOT}/../../scripts/lib/config-loader.sh"
|
|
11
|
+
# config_load_plugin "bursar" "$repo_root" "_BURSAR_CONFIG"
|
|
12
|
+
# config_get "_BURSAR_CONFIG" '.bursar.window' # returns value or empty string
|
|
13
|
+
#
|
|
14
|
+
# Precedence (latest wins):
|
|
15
|
+
# 1. plugin config.json (shipped defaults)
|
|
16
|
+
# 2. ~/.claude/settings.json
|
|
17
|
+
# 3. ~/.claude/settings.local.json (local overrides user)
|
|
18
|
+
# 4. <repo>/.claude/settings.json
|
|
19
|
+
# 5. <repo>/.claude/settings.local.json (local overrides project)
|
|
20
|
+
|
|
21
|
+
# Load config for a plugin, merging all five layers into a variable.
|
|
22
|
+
#
|
|
23
|
+
# Arguments:
|
|
24
|
+
# $1 = plugin name (e.g., "bursar", "compass")
|
|
25
|
+
# $2 = repo root (or empty for no-repo defaults)
|
|
26
|
+
# $3 = output variable name (e.g., "_BURSAR_CONFIG")
|
|
27
|
+
#
|
|
28
|
+
# Sets the output variable to the merged JSON config.
|
|
29
|
+
config_load_plugin() {
|
|
30
|
+
local plugin_name="${1:-}"
|
|
31
|
+
local repo_root="${2:-}"
|
|
32
|
+
local output_var="${3:-}"
|
|
33
|
+
|
|
34
|
+
[[ -z "$plugin_name" || -z "$output_var" ]] && return 1
|
|
35
|
+
|
|
36
|
+
local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
|
|
37
|
+
local home_dir="${HOME:-}"
|
|
38
|
+
|
|
39
|
+
# Read all five layers as raw text using $(<file) to avoid process forks.
|
|
40
|
+
# Missing files degrade to empty strings (handled by jq with //).
|
|
41
|
+
local default_txt="" home_txt="" home_local_txt="" repo_txt="" repo_local_txt=""
|
|
42
|
+
local default_file="${plugin_root}/config.json"
|
|
43
|
+
local home_file="${home_dir}/.claude/settings.json"
|
|
44
|
+
local home_local_file="${home_dir}/.claude/settings.local.json"
|
|
45
|
+
local repo_file=""
|
|
46
|
+
local repo_local_file=""
|
|
47
|
+
|
|
48
|
+
[[ -n "$repo_root" ]] && repo_file="${repo_root}/.claude/settings.json"
|
|
49
|
+
[[ -n "$repo_root" ]] && repo_local_file="${repo_root}/.claude/settings.local.json"
|
|
50
|
+
|
|
51
|
+
# Read each layer defensively—missing or malformed files degrade to empty.
|
|
52
|
+
[[ -f "$default_file" ]] && default_txt="$(<"$default_file")"
|
|
53
|
+
[[ -f "$home_file" ]] && home_txt="$(<"$home_file")"
|
|
54
|
+
[[ -f "$home_local_file" ]] && home_local_txt="$(<"$home_local_file")"
|
|
55
|
+
[[ -f "$repo_file" ]] && repo_txt="$(<"$repo_file")"
|
|
56
|
+
[[ -f "$repo_local_file" ]] && repo_local_txt="$(<"$repo_local_file")"
|
|
57
|
+
|
|
58
|
+
# Merge all five layers in a single jq invocation. Precedence:
|
|
59
|
+
# defaults < home < home-local < repo < repo-local
|
|
60
|
+
# Settings files (.json, .local.json) contribute only their plugin-scoped key.
|
|
61
|
+
local merged_json
|
|
62
|
+
merged_json=$(jq -n \
|
|
63
|
+
--arg plugin "$plugin_name" \
|
|
64
|
+
--arg d "$default_txt" \
|
|
65
|
+
--arg h "$home_txt" \
|
|
66
|
+
--arg hl "$home_local_txt" \
|
|
67
|
+
--arg r "$repo_txt" \
|
|
68
|
+
--arg rl "$repo_local_txt" \
|
|
69
|
+
'
|
|
70
|
+
def deepmerge($a; $b):
|
|
71
|
+
if ($a|type) == "object" and ($b|type) == "object" then
|
|
72
|
+
reduce (($a|keys) + ($b|keys) | unique)[] as $k
|
|
73
|
+
({}; .[$k] = deepmerge($a[$k]; $b[$k]))
|
|
74
|
+
elif $b == null then $a
|
|
75
|
+
else $b end;
|
|
76
|
+
|
|
77
|
+
($d | fromjson? // {}) as $defaults
|
|
78
|
+
| (($h | fromjson? // {}) | {($plugin): (.[$plugin] // {})}) as $home
|
|
79
|
+
| (($hl | fromjson? // {}) | {($plugin): (.[$plugin] // {})}) as $home_local
|
|
80
|
+
| (($r | fromjson? // {}) | {($plugin): (.[$plugin] // {})}) as $repo
|
|
81
|
+
| (($rl | fromjson? // {}) | {($plugin): (.[$plugin] // {})}) as $repo_local
|
|
82
|
+
| deepmerge(
|
|
83
|
+
deepmerge(
|
|
84
|
+
deepmerge(
|
|
85
|
+
deepmerge($defaults; $home);
|
|
86
|
+
$home_local);
|
|
87
|
+
$repo);
|
|
88
|
+
$repo_local)
|
|
89
|
+
' 2>/dev/null) || merged_json="{}"
|
|
90
|
+
|
|
91
|
+
[[ -z "$merged_json" ]] && merged_json="{}"
|
|
92
|
+
|
|
93
|
+
# Set the output variable in the caller's scope via printf (works in bash).
|
|
94
|
+
printf -v "$output_var" '%s' "$merged_json"
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Get a string value from loaded config.
|
|
99
|
+
#
|
|
100
|
+
# Arguments:
|
|
101
|
+
# $1 = variable name containing the config JSON (e.g., "_BURSAR_CONFIG")
|
|
102
|
+
# $2 = jq path to the value (e.g., '.bursar.window')
|
|
103
|
+
#
|
|
104
|
+
# Outputs: the string value, or empty string if not found.
|
|
105
|
+
config_get() {
|
|
106
|
+
local config_var="${1:-}"
|
|
107
|
+
local path="${2:-}"
|
|
108
|
+
|
|
109
|
+
[[ -z "$config_var" ]] && return 1
|
|
110
|
+
|
|
111
|
+
# Use indirect expansion to read the variable's value.
|
|
112
|
+
local config_json="${!config_var}"
|
|
113
|
+
printf '%s' "$config_json" | jq -r "${path} // empty" 2>/dev/null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Get a JSON value from loaded config.
|
|
117
|
+
#
|
|
118
|
+
# Arguments:
|
|
119
|
+
# $1 = variable name containing the config JSON (e.g., "_BURSAR_CONFIG")
|
|
120
|
+
# $2 = jq path to the value (e.g., '.bursar.markers')
|
|
121
|
+
#
|
|
122
|
+
# Outputs: the JSON value, or null if not found.
|
|
123
|
+
config_get_json() {
|
|
124
|
+
local config_var="${1:-}"
|
|
125
|
+
local path="${2:-}"
|
|
126
|
+
|
|
127
|
+
[[ -z "$config_var" ]] && return 1
|
|
128
|
+
|
|
129
|
+
local config_json="${!config_var}"
|
|
130
|
+
printf '%s' "$config_json" | jq -c "${path}" 2>/dev/null
|
|
131
|
+
}
|