@onlooker-community/ecosystem 0.33.1 → 0.34.1
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/.agents/skills/beads/SKILL.md +80 -0
- package/.agents/skills/beads/agents/openai.yaml +4 -0
- package/.claude/settings.json +13 -0
- package/.claude/skills/writing-tests/SKILL.md +27 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex/config.toml +2 -0
- package/.codex/hooks.json +51 -0
- package/.markdownlint.json +3 -0
- package/.release-please-manifest.json +6 -6
- package/AGENTS.md +246 -0
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +56 -1
- package/docs/lesson-promotion-pipeline.md +210 -0
- package/docs/superpowers/plans/2026-08-09-lesson-transform.md +1537 -0
- package/docs/superpowers/specs/2026-08-09-lesson-transform-design.md +261 -0
- package/package.json +3 -2
- package/plugins/assayer/.claude-plugin/plugin.json +1 -1
- package/plugins/assayer/CHANGELOG.md +7 -0
- package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
- package/plugins/curator/.claude-plugin/plugin.json +1 -1
- package/plugins/curator/CHANGELOG.md +7 -0
- package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
- package/plugins/historian/.claude-plugin/plugin.json +1 -1
- package/plugins/historian/CHANGELOG.md +7 -0
- package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
- package/plugins/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +14 -0
- package/plugins/librarian/config.json +4 -0
- package/plugins/librarian/schema/PROVENANCE.json +7 -0
- package/plugins/librarian/schema/lesson-applies-to.subschema.json +74 -0
- package/plugins/librarian/schema/lesson-evidence.subschema.json +36 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +26 -0
- package/plugins/librarian/scripts/lib/librarian-cli.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +135 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +311 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +140 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
- package/plugins/tribunal/CHANGELOG.md +7 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +3 -1
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +2 -1
- package/scripts/lib/prompt-rules.sh +6 -1
- package/scripts/lint/check-lesson-schema-drift.mjs +36 -0
- package/test/bats/archivist-inject.bats +1 -1
- package/test/bats/assayer-extract.bats +2 -2
- package/test/bats/bursar-session-start.bats +3 -3
- package/test/bats/cartographer-lock.bats +3 -3
- package/test/bats/compass-sanitizer.bats +11 -11
- package/test/bats/compass-transcript.bats +2 -2
- package/test/bats/config.bats +15 -15
- package/test/bats/curator-session-start.bats +10 -3
- package/test/bats/emit-payload-default.bats +52 -0
- package/test/bats/governor-ledger.bats +1 -1
- package/test/bats/historian-prompt-submit.bats +1 -1
- package/test/bats/inspector-post-write-hook.bats +4 -4
- package/test/bats/librarian-cli.bats +16 -16
- package/test/bats/librarian-lesson-transform.bats +609 -0
- package/test/bats/librarian-session-start.bats +2 -2
- package/test/bats/lineage-config.bats +1 -1
- package/test/bats/lineage-redact.bats +5 -5
- package/test/bats/session-tracker.bats +4 -4
- package/test/bats/tribunal-jury.bats +1 -1
- package/test/bats/turn-tracker.bats +1 -1
- package/test/bats/warden-sanitizer.bats +3 -3
- package/test/bats/worktree-tracker.bats +2 -2
- package/test/node/lesson-schema-drift.test.mjs +28 -0
- package/test/node/lesson-validate-agreement.test.mjs +154 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Storage for the lesson subtree.
|
|
3
|
+
#
|
|
4
|
+
# <project_dir>/lessons/proposals/<ulid>.json awaiting human confirmation
|
|
5
|
+
# <project_dir>/lessons/approved/<ulid>.json jury passed (written by 4z8.4)
|
|
6
|
+
# <project_dir>/lessons/declined.jsonl append-only, never re-judged
|
|
7
|
+
#
|
|
8
|
+
# Lessons live apart from librarian's memory `proposals/` on purpose: a memory
|
|
9
|
+
# promotion writes to this machine, a lesson proposal is a step toward
|
|
10
|
+
# publishing beyond it. Separate trees keep a confirmation surface from
|
|
11
|
+
# merging the two by accident.
|
|
12
|
+
#
|
|
13
|
+
# Requires librarian-storage.sh (librarian_project_dir) and librarian-ulid.sh.
|
|
14
|
+
|
|
15
|
+
librarian_lessons_dir() {
|
|
16
|
+
local key="$1"
|
|
17
|
+
printf '%s/lessons' "$(librarian_project_dir "$key")"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
librarian_lesson_storage_init() {
|
|
21
|
+
local key="$1"
|
|
22
|
+
[[ -z "$key" ]] && return 1
|
|
23
|
+
local dir
|
|
24
|
+
dir=$(librarian_lessons_dir "$key")
|
|
25
|
+
mkdir -p "$dir/proposals" "$dir/approved" 2>/dev/null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Write one candidate. Prints the ULID on success.
|
|
29
|
+
# Usage: librarian_lesson_write_proposal <key> <candidate_json> <artifact_id>
|
|
30
|
+
librarian_lesson_write_proposal() {
|
|
31
|
+
local key="$1"
|
|
32
|
+
local candidate="$2"
|
|
33
|
+
local artifact_id="$3"
|
|
34
|
+
[[ -z "$key" || -z "$candidate" || -z "$artifact_id" ]] && return 1
|
|
35
|
+
|
|
36
|
+
librarian_lesson_storage_init "$key" || return 1
|
|
37
|
+
|
|
38
|
+
local id now out
|
|
39
|
+
id=$(librarian_ulid) || return 1
|
|
40
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
41
|
+
out="$(librarian_lessons_dir "$key")/proposals/${id}.json"
|
|
42
|
+
|
|
43
|
+
jq -n \
|
|
44
|
+
--arg id "$id" \
|
|
45
|
+
--arg artifact_id "$artifact_id" \
|
|
46
|
+
--arg created "$now" \
|
|
47
|
+
--argjson candidate "$candidate" \
|
|
48
|
+
'{
|
|
49
|
+
id: $id,
|
|
50
|
+
artifact_id: $artifact_id,
|
|
51
|
+
created_at: $created,
|
|
52
|
+
status: "pending",
|
|
53
|
+
candidate: $candidate
|
|
54
|
+
}' > "$out" 2>/dev/null || return 1
|
|
55
|
+
|
|
56
|
+
printf '%s' "$id"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Append one decline. Only ever called for real determinations — never for a
|
|
60
|
+
# missing CLI, a timeout, or an empty response. Recording an outage here would
|
|
61
|
+
# bury a good artifact permanently, because the watermark has already moved
|
|
62
|
+
# past it and declined entries are never re-read.
|
|
63
|
+
#
|
|
64
|
+
# Usage: librarian_lesson_append_declined <key> <artifact_id> <reason> [detail]
|
|
65
|
+
librarian_lesson_append_declined() {
|
|
66
|
+
local key="$1"
|
|
67
|
+
local artifact_id="$2"
|
|
68
|
+
local reason="$3"
|
|
69
|
+
local detail="${4:-}"
|
|
70
|
+
[[ -z "$key" || -z "$artifact_id" || -z "$reason" ]] && return 1
|
|
71
|
+
|
|
72
|
+
librarian_lesson_storage_init "$key" || return 1
|
|
73
|
+
|
|
74
|
+
local now line
|
|
75
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
76
|
+
line=$(jq -cn \
|
|
77
|
+
--arg artifact_id "$artifact_id" \
|
|
78
|
+
--arg reason "$reason" \
|
|
79
|
+
--arg detail "$detail" \
|
|
80
|
+
--arg at "$now" \
|
|
81
|
+
'{
|
|
82
|
+
artifact_id: $artifact_id,
|
|
83
|
+
reason: $reason,
|
|
84
|
+
detail: (if $detail == "" then null else $detail end),
|
|
85
|
+
declined_at: $at
|
|
86
|
+
}') || return 1
|
|
87
|
+
|
|
88
|
+
printf '%s\n' "$line" >> "$(librarian_lessons_dir "$key")/declined.jsonl"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Returns 0 when this artifact has already been handled.
|
|
92
|
+
#
|
|
93
|
+
# The watermark cannot answer this: last_scan.json records only *when* we
|
|
94
|
+
# scanned, not which artifacts were considered. Idempotency is artifact-keyed
|
|
95
|
+
# and permanent, unlike tombstones (body-hash keyed, TTL'd).
|
|
96
|
+
#
|
|
97
|
+
# Usage: librarian_lesson_seen <key> <artifact_id>
|
|
98
|
+
librarian_lesson_seen() {
|
|
99
|
+
local key="$1"
|
|
100
|
+
local artifact_id="$2"
|
|
101
|
+
[[ -z "$key" || -z "$artifact_id" ]] && return 1
|
|
102
|
+
|
|
103
|
+
local dir
|
|
104
|
+
dir=$(librarian_lessons_dir "$key")
|
|
105
|
+
|
|
106
|
+
# -R reads each line as a raw string and fromjson? yields nothing for a
|
|
107
|
+
# line that fails to parse, instead of aborting the whole jq invocation.
|
|
108
|
+
# Without this, one truncated trailing line (e.g. a process killed
|
|
109
|
+
# mid-append) makes jq exit 5 for the entire file, and every artifact
|
|
110
|
+
# declined before that line reads back as "not seen."
|
|
111
|
+
#
|
|
112
|
+
# `objects` after fromjson? is load-bearing, not decorative: fromjson?
|
|
113
|
+
# only guards the *parse*, not what comes after it in the pipe. A line
|
|
114
|
+
# that is valid JSON but not an object (a bare `123`, `true`, `"str"`, or
|
|
115
|
+
# `[1,2,3]`) parses cleanly, then `.artifact_id` indexing on that
|
|
116
|
+
# non-object errors out the whole jq invocation — the same
|
|
117
|
+
# every-prior-decline-reads-as-unseen failure the -R/fromjson? guard
|
|
118
|
+
# above exists to prevent, just reached through a different door.
|
|
119
|
+
# `objects` filters those values out before `.artifact_id` ever runs.
|
|
120
|
+
if [[ -f "$dir/declined.jsonl" ]] \
|
|
121
|
+
&& jq -Re --arg a "$artifact_id" 'fromjson? | objects | select(.artifact_id == $a)' \
|
|
122
|
+
"$dir/declined.jsonl" >/dev/null 2>&1; then
|
|
123
|
+
return 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
local f
|
|
127
|
+
for f in "$dir"/proposals/*.json "$dir"/approved/*.json; do
|
|
128
|
+
[[ -f "$f" ]] || continue
|
|
129
|
+
if jq -e --arg a "$artifact_id" '.artifact_id == $a' "$f" >/dev/null 2>&1; then
|
|
130
|
+
return 0
|
|
131
|
+
fi
|
|
132
|
+
done
|
|
133
|
+
|
|
134
|
+
return 1
|
|
135
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Lesson transform — librarian's fifth stage.
|
|
3
|
+
#
|
|
4
|
+
# Reads one durable, classified, deduped archivist artifact and emits a lesson
|
|
5
|
+
# candidate: the four fields inferable from an artifact (claim, rationale,
|
|
6
|
+
# evidence, applies_to). The other nine required Lesson fields belong to later
|
|
7
|
+
# stages, so this never produces a schema-complete Lesson and cannot be
|
|
8
|
+
# validated against the full lesson schema.
|
|
9
|
+
#
|
|
10
|
+
# Requires librarian-lesson-validate.sh, librarian-lesson-storage.sh, and
|
|
11
|
+
# librarian-config.sh (librarian_config_get).
|
|
12
|
+
#
|
|
13
|
+
# Config inputs (read via librarian_config_get from librarian_lesson_call):
|
|
14
|
+
# librarian.lesson_transform.model Anthropic model id
|
|
15
|
+
# librarian.lesson_transform.timeout_seconds Hard wall-clock ceiling
|
|
16
|
+
|
|
17
|
+
# Fallback when config hasn't been loaded or leaves the key unset.
|
|
18
|
+
_LIBRARIAN_LESSON_DEFAULT_TIMEOUT_SECONDS=20
|
|
19
|
+
|
|
20
|
+
# Cap on how much of a response the JSON-object extractor will scan. The
|
|
21
|
+
# scanner is a per-character bash loop — effectively O(n^2) on long input,
|
|
22
|
+
# since bash string slicing on a long string isn't O(1) per call — and it
|
|
23
|
+
# runs after the claude call, uncapped, inside a SessionEnd hook that must
|
|
24
|
+
# not stall session end. `claude -p` has no output-size flag to bound the
|
|
25
|
+
# response itself, so the bound is enforced here instead. A response that
|
|
26
|
+
# exceeds this without yielding valid JSON in the scanned prefix has not
|
|
27
|
+
# followed the "output ONLY a single JSON object on one line" instruction
|
|
28
|
+
# anyway, so declining it is correct, not just expedient.
|
|
29
|
+
_LIBRARIAN_LESSON_EXTRACT_MAX_CHARS=8192
|
|
30
|
+
|
|
31
|
+
# Usage: librarian_lesson_build_prompt <artifact_json>
|
|
32
|
+
librarian_lesson_build_prompt() {
|
|
33
|
+
local artifact="$1"
|
|
34
|
+
local summary detail files_list artifact_id session_id project_key created_at
|
|
35
|
+
|
|
36
|
+
summary=$(printf '%s' "$artifact" | jq -r '.summary // ""')
|
|
37
|
+
detail=$(printf '%s' "$artifact" | jq -r '.detail // ""')
|
|
38
|
+
files_list=$(printf '%s' "$artifact" | jq -r '(.files // []) | join(", ")')
|
|
39
|
+
artifact_id=$(printf '%s' "$artifact" | jq -r '.id // ""')
|
|
40
|
+
session_id=$(printf '%s' "$artifact" | jq -r '.session_id // ""')
|
|
41
|
+
project_key=$(printf '%s' "$artifact" | jq -r '.project_key // ""')
|
|
42
|
+
created_at=$(printf '%s' "$artifact" | jq -r '.created_at // ""')
|
|
43
|
+
|
|
44
|
+
cat <<EOF
|
|
45
|
+
You are turning a session artifact into a shareable lesson, or refusing to.
|
|
46
|
+
|
|
47
|
+
A lesson states something that was learned, why it follows, and the exact
|
|
48
|
+
version range in which it holds. It is shared with other people, so a wrong
|
|
49
|
+
lesson actively misleads. Refusing is the safe answer.
|
|
50
|
+
|
|
51
|
+
Output ONLY one JSON object on one line. No markdown fences, no prose.
|
|
52
|
+
|
|
53
|
+
REFUSE when either is true, by outputting exactly:
|
|
54
|
+
{ "eligible": false, "reason": "no_resolution" }
|
|
55
|
+
{ "eligible": false, "reason": "no_versions" }
|
|
56
|
+
|
|
57
|
+
- "no_resolution": the artifact records a problem but not what resolved it.
|
|
58
|
+
"This breaks" without "and this fixed it" is a warning, not a lesson.
|
|
59
|
+
Never invent a resolution that is not in the artifact.
|
|
60
|
+
- "no_versions": you cannot determine which versions the claim is bound to.
|
|
61
|
+
|
|
62
|
+
Otherwise output:
|
|
63
|
+
{
|
|
64
|
+
"claim": "<what was learned, one sentence>",
|
|
65
|
+
"rationale": "<why the claim follows from the evidence>",
|
|
66
|
+
"evidence": { "resolution": "<what actually resolved it, from the artifact>" },
|
|
67
|
+
"applies_to": {
|
|
68
|
+
"stack": ["<tool or package name>", "<another tool or package name>"],
|
|
69
|
+
"scope": { "kind": "versioned", "versions": { "<stack entry>": "<range>" } },
|
|
70
|
+
"file_patterns": [],
|
|
71
|
+
"task_kinds": []
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
VERSION RANGE RULES — these are strict and a violation is discarded:
|
|
76
|
+
- Allowed: "<6", "<=6", "=6", ">4", ">=4", or two-sided ">=4 <6".
|
|
77
|
+
- FORBIDDEN: npm syntax. Never "^5.4.21", "~5", "5.x", or a bare "5.4.21".
|
|
78
|
+
- FORBIDDEN: ">=0", ">=0.0", ">=0.0.0". An unbounded lower bound matches
|
|
79
|
+
everything and would never expire.
|
|
80
|
+
- Every key in versions MUST also appear in stack.
|
|
81
|
+
- Generalize honestly. Observing a break on vite 5.4.21 with vitest 4.1.9
|
|
82
|
+
supports {"vite": "<6", "vitest": ">=4"} only if the cause is the missing
|
|
83
|
+
API rather than that exact build.
|
|
84
|
+
|
|
85
|
+
There is no version-independent option. If the claim is not bound to a
|
|
86
|
+
version range, refuse with "no_versions".
|
|
87
|
+
|
|
88
|
+
<artifact>
|
|
89
|
+
id: ${artifact_id}
|
|
90
|
+
summary: ${summary}
|
|
91
|
+
detail: ${detail}
|
|
92
|
+
files: ${files_list}
|
|
93
|
+
project_key: ${project_key}
|
|
94
|
+
session_id: ${session_id}
|
|
95
|
+
created_at: ${created_at}
|
|
96
|
+
</artifact>
|
|
97
|
+
EOF
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Extract the first balanced top-level JSON object from a string that may
|
|
101
|
+
# carry surrounding prose ("Here is the JSON: {...}"). Prints the substring
|
|
102
|
+
# on success, prints nothing and returns 1 on failure. Depth-tracks braces
|
|
103
|
+
# while skipping ones inside string literals (honoring backslash escapes),
|
|
104
|
+
# so a claim like `{"claim": "uses \"quotes\" and { in prose"}` still
|
|
105
|
+
# extracts correctly.
|
|
106
|
+
#
|
|
107
|
+
# Prose wrapping is not the same failure as unparseable output: a model that
|
|
108
|
+
# added a sentence around otherwise-valid JSON would very likely produce
|
|
109
|
+
# clean JSON on a resample, so declining it as transform_invalid would bury
|
|
110
|
+
# a good artifact over formatting noise rather than a real judgment problem.
|
|
111
|
+
#
|
|
112
|
+
# Usage: _librarian_lesson_extract_json_object <text>
|
|
113
|
+
_librarian_lesson_extract_json_object() {
|
|
114
|
+
local text="$1"
|
|
115
|
+
local start=-1 depth=0 in_string=0 escape=0
|
|
116
|
+
local i len ch
|
|
117
|
+
|
|
118
|
+
len=${#text}
|
|
119
|
+
for (( i = 0; i < len; i++ )); do
|
|
120
|
+
ch="${text:i:1}"
|
|
121
|
+
if [[ $start -eq -1 ]]; then
|
|
122
|
+
[[ "$ch" == "{" ]] && { start=$i; depth=1; }
|
|
123
|
+
continue
|
|
124
|
+
fi
|
|
125
|
+
if [[ $escape -eq 1 ]]; then
|
|
126
|
+
escape=0
|
|
127
|
+
continue
|
|
128
|
+
fi
|
|
129
|
+
case "$ch" in
|
|
130
|
+
'\') [[ $in_string -eq 1 ]] && escape=1 ;;
|
|
131
|
+
'"') in_string=$((1 - in_string)) ;;
|
|
132
|
+
'{') [[ $in_string -eq 0 ]] && depth=$((depth + 1)) ;;
|
|
133
|
+
'}')
|
|
134
|
+
if [[ $in_string -eq 0 ]]; then
|
|
135
|
+
depth=$((depth - 1))
|
|
136
|
+
if [[ $depth -eq 0 ]]; then
|
|
137
|
+
printf '%s' "${text:start:i-start+1}"
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
;;
|
|
142
|
+
esac
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Call the model. Prints raw output, or empty string on ANY infrastructure
|
|
149
|
+
# failure — missing CLI, timeout, empty response. Empty means "could not
|
|
150
|
+
# judge", which is not a verdict.
|
|
151
|
+
#
|
|
152
|
+
# Usage: librarian_lesson_call <artifact_json> <model>
|
|
153
|
+
librarian_lesson_call() {
|
|
154
|
+
local artifact="$1"
|
|
155
|
+
local model="${2:-}"
|
|
156
|
+
|
|
157
|
+
command -v claude >/dev/null 2>&1 || return 0
|
|
158
|
+
[[ -z "$artifact" ]] && return 0
|
|
159
|
+
|
|
160
|
+
local prompt_file
|
|
161
|
+
prompt_file=$(mktemp -t librarian-lesson.XXXXXX 2>/dev/null) \
|
|
162
|
+
|| prompt_file="/tmp/librarian-lesson.$$"
|
|
163
|
+
# shellcheck disable=SC2064
|
|
164
|
+
trap "rm -f '$prompt_file'" EXIT
|
|
165
|
+
|
|
166
|
+
librarian_lesson_build_prompt "$artifact" > "$prompt_file" || return 0
|
|
167
|
+
|
|
168
|
+
local args=(-p --max-turns 1)
|
|
169
|
+
[[ -n "$model" ]] && args+=(--model "$model")
|
|
170
|
+
|
|
171
|
+
local timeout_seconds
|
|
172
|
+
timeout_seconds=$(librarian_config_get '.librarian.lesson_transform.timeout_seconds' 2>/dev/null)
|
|
173
|
+
[[ -z "$timeout_seconds" || "$timeout_seconds" == "null" ]] \
|
|
174
|
+
&& timeout_seconds="$_LIBRARIAN_LESSON_DEFAULT_TIMEOUT_SECONDS"
|
|
175
|
+
|
|
176
|
+
local response=""
|
|
177
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
178
|
+
response=$(timeout "$timeout_seconds" \
|
|
179
|
+
claude "${args[@]}" < "$prompt_file" 2>/dev/null) || response=""
|
|
180
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
181
|
+
response=$(gtimeout "$timeout_seconds" \
|
|
182
|
+
claude "${args[@]}" < "$prompt_file" 2>/dev/null) || response=""
|
|
183
|
+
else
|
|
184
|
+
response=$(claude "${args[@]}" < "$prompt_file" 2>/dev/null) || response=""
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
rm -f "$prompt_file"
|
|
188
|
+
trap - EXIT
|
|
189
|
+
|
|
190
|
+
[[ -z "$response" ]] && return 0
|
|
191
|
+
|
|
192
|
+
local cleaned
|
|
193
|
+
cleaned=$(printf '%s' "$response" | sed -e 's/^```json//' -e 's/^```//' -e 's/```$//')
|
|
194
|
+
|
|
195
|
+
# Fast path: the response is already valid JSON on its own.
|
|
196
|
+
if printf '%s' "$cleaned" | jq -e . >/dev/null 2>&1; then
|
|
197
|
+
printf '%s' "$cleaned"
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# Slow path: pull the first balanced JSON object out of surrounding
|
|
202
|
+
# prose. Bounded to a fixed prefix (see _LIBRARIAN_LESSON_EXTRACT_MAX_CHARS)
|
|
203
|
+
# so a rambling, arbitrarily long response can't turn the O(n^2) scan
|
|
204
|
+
# into an unbounded stall. Only used when it actually recovers valid
|
|
205
|
+
# JSON — otherwise fall through to the original text so the
|
|
206
|
+
# unparseable case still declines.
|
|
207
|
+
local extracted
|
|
208
|
+
extracted=$(_librarian_lesson_extract_json_object \
|
|
209
|
+
"${cleaned:0:_LIBRARIAN_LESSON_EXTRACT_MAX_CHARS}")
|
|
210
|
+
if [[ -n "$extracted" ]] && printf '%s' "$extracted" | jq -e . >/dev/null 2>&1; then
|
|
211
|
+
printf '%s' "$extracted"
|
|
212
|
+
return 0
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
printf '%s' "$cleaned"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Transform one artifact. Always exits 0. Prints exactly one of:
|
|
219
|
+
# proposed:<ulid> candidate written
|
|
220
|
+
# declined:<reason> a real verdict, recorded in declined.jsonl
|
|
221
|
+
# skipped:pregate no version token; free to redo, nothing recorded
|
|
222
|
+
# skipped:seen already handled
|
|
223
|
+
# unavailable infrastructure failure; nothing recorded
|
|
224
|
+
#
|
|
225
|
+
# Usage: librarian_lesson_transform_one <key> <artifact_json>
|
|
226
|
+
librarian_lesson_transform_one() {
|
|
227
|
+
local key="$1"
|
|
228
|
+
local artifact="$2"
|
|
229
|
+
[[ -z "$key" || -z "$artifact" ]] && { printf 'unavailable'; return 0; }
|
|
230
|
+
|
|
231
|
+
local artifact_id session_id project_key created_at
|
|
232
|
+
artifact_id=$(printf '%s' "$artifact" | jq -r '.id // ""')
|
|
233
|
+
session_id=$(printf '%s' "$artifact" | jq -r '.session_id // ""')
|
|
234
|
+
project_key=$(printf '%s' "$artifact" | jq -r '.project_key // ""')
|
|
235
|
+
created_at=$(printf '%s' "$artifact" | jq -r '.created_at // ""')
|
|
236
|
+
[[ -z "$artifact_id" ]] && { printf 'unavailable'; return 0; }
|
|
237
|
+
|
|
238
|
+
if librarian_lesson_seen "$key" "$artifact_id"; then
|
|
239
|
+
printf 'skipped:seen'
|
|
240
|
+
return 0
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
if ! librarian_lesson_pregate "$artifact"; then
|
|
244
|
+
printf 'skipped:pregate'
|
|
245
|
+
return 0
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
local model raw
|
|
249
|
+
model=$(librarian_config_get '.librarian.lesson_transform.model')
|
|
250
|
+
|
|
251
|
+
raw=$(librarian_lesson_call "$artifact" "$model")
|
|
252
|
+
|
|
253
|
+
# Empty means infrastructure, not verdict. Leave the artifact untouched.
|
|
254
|
+
if [[ -z "$raw" ]]; then
|
|
255
|
+
printf 'unavailable'
|
|
256
|
+
return 0
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
if ! printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then
|
|
260
|
+
librarian_lesson_append_declined "$key" "$artifact_id" "transform_invalid"
|
|
261
|
+
printf 'declined:transform_invalid'
|
|
262
|
+
return 0
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
# An explicit refusal is a real answer. Checked with jq -e rather than a
|
|
266
|
+
# `// empty` string capture: jq's // operator treats JSON `false` as
|
|
267
|
+
# falsy, same as null, so `.eligible // empty` silently discards a real
|
|
268
|
+
# `"eligible": false` refusal instead of reporting it.
|
|
269
|
+
local reason
|
|
270
|
+
if printf '%s' "$raw" | jq -e '.eligible == false' >/dev/null 2>&1; then
|
|
271
|
+
reason=$(printf '%s' "$raw" | jq -r '.reason // "transform_invalid"')
|
|
272
|
+
case "$reason" in
|
|
273
|
+
no_resolution|no_versions) ;;
|
|
274
|
+
*) reason="transform_invalid" ;;
|
|
275
|
+
esac
|
|
276
|
+
librarian_lesson_append_declined "$key" "$artifact_id" "$reason"
|
|
277
|
+
printf 'declined:%s' "$reason"
|
|
278
|
+
return 0
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Stitch in the provenance the model is not asked to produce.
|
|
282
|
+
local candidate
|
|
283
|
+
candidate=$(printf '%s' "$raw" | jq -c \
|
|
284
|
+
--arg aid "$artifact_id" \
|
|
285
|
+
--arg sid "$session_id" \
|
|
286
|
+
--arg pk "$project_key" \
|
|
287
|
+
--arg at "$created_at" \
|
|
288
|
+
'.evidence.artifact_ids = [$aid]
|
|
289
|
+
| .evidence.session_ids = [$sid]
|
|
290
|
+
| .evidence.project_key = $pk
|
|
291
|
+
| .evidence.observed_at = $at' 2>/dev/null) || candidate=""
|
|
292
|
+
|
|
293
|
+
if [[ -z "$candidate" ]]; then
|
|
294
|
+
librarian_lesson_append_declined "$key" "$artifact_id" "transform_invalid"
|
|
295
|
+
printf 'declined:transform_invalid'
|
|
296
|
+
return 0
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if ! librarian_lesson_validate_candidate "$candidate" 2>/dev/null; then
|
|
300
|
+
librarian_lesson_append_declined "$key" "$artifact_id" "schema_invalid"
|
|
301
|
+
printf 'declined:schema_invalid'
|
|
302
|
+
return 0
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
local id
|
|
306
|
+
id=$(librarian_lesson_write_proposal "$key" "$candidate" "$artifact_id") || {
|
|
307
|
+
printf 'unavailable'
|
|
308
|
+
return 0
|
|
309
|
+
}
|
|
310
|
+
printf 'proposed:%s' "$id"
|
|
311
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Pure validation rules for lesson candidates. No I/O, no network, no CLI.
|
|
3
|
+
#
|
|
4
|
+
# These rules mirror the vendored sub-schemas in plugins/librarian/schema/.
|
|
5
|
+
# ajv cannot run at runtime (installed plugins ship no node_modules, ADR-005),
|
|
6
|
+
# so enforcement here is jq. The two mechanisms have been proven able to
|
|
7
|
+
# disagree, so tests assert them separately.
|
|
8
|
+
|
|
9
|
+
# Version-shaped token check. Returns 0 when the artifact could plausibly
|
|
10
|
+
# yield a versioned scope, 1 when it definitionally cannot.
|
|
11
|
+
#
|
|
12
|
+
# This rejects only what is impossible, never what is merely low quality:
|
|
13
|
+
# the transform can emit `versioned` scope alone, so an artifact with no
|
|
14
|
+
# version token anywhere cannot produce a valid scope.versions.
|
|
15
|
+
#
|
|
16
|
+
# Usage: librarian_lesson_pregate <artifact_json>
|
|
17
|
+
librarian_lesson_pregate() {
|
|
18
|
+
local artifact="${1:-}"
|
|
19
|
+
[[ -z "$artifact" ]] && return 1
|
|
20
|
+
|
|
21
|
+
local text
|
|
22
|
+
text=$(printf '%s' "$artifact" | jq -r '((.summary // "") + " " + (.detail // ""))' 2>/dev/null) || return 1
|
|
23
|
+
[[ -z "$text" ]] && return 1
|
|
24
|
+
|
|
25
|
+
# Dotted (5.4.21), v-prefixed (v5), or x-range (5.x).
|
|
26
|
+
printf '%s' "$text" | grep -qE '([0-9]+\.[0-9]+)|(\bv[0-9]+)|([0-9]+\.x\b)'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Version range check, mirroring the vendored pattern.
|
|
30
|
+
#
|
|
31
|
+
# Accepts: <6 <=6 =6 >4 >=4 ">=4 <6"
|
|
32
|
+
# Rejects: ^5.4.21 ~5 5.x 5.4.21 >=0 >=0.0.0
|
|
33
|
+
#
|
|
34
|
+
# The >= and > forms require a non-zero lower bound. An unbounded lower bound
|
|
35
|
+
# matches every session and would never expire — version independence in
|
|
36
|
+
# disguise, which this stage is not allowed to mint.
|
|
37
|
+
#
|
|
38
|
+
# Usage: librarian_lesson_valid_range <string>
|
|
39
|
+
librarian_lesson_valid_range() {
|
|
40
|
+
local r="${1:-}"
|
|
41
|
+
[[ -z "$r" ]] && return 1
|
|
42
|
+
|
|
43
|
+
# The integer-part alternative is [0-9]*[1-9][0-9]*, not [1-9][0-9]*: the
|
|
44
|
+
# vendored pattern's equivalent is \d*[1-9]\d*, which allows a leading
|
|
45
|
+
# zero digit (05, 007) as long as some digit is nonzero. [1-9][0-9]* only
|
|
46
|
+
# permits a nonzero *leading* digit, so it rejects >=05 while the vendored
|
|
47
|
+
# schema accepts it — a real jq/schema disagreement, not a style choice.
|
|
48
|
+
local nonzero='([0-9]*[1-9][0-9]*(\.[0-9]+)?(\.[0-9]+)?|0+\.[0-9]*[1-9][0-9]*(\.[0-9]+)?|0+\.0+\.[0-9]*[1-9][0-9]*)'
|
|
49
|
+
local any='[0-9]+(\.[0-9]+)?(\.[0-9]+)?'
|
|
50
|
+
local pattern="^((<|<=|=)${any}|(>|>=)${nonzero}|(>|>=)${any} (<|<=)${any})$"
|
|
51
|
+
|
|
52
|
+
# Use bash's own regex engine rather than grep: grep's ^/$ anchor to line
|
|
53
|
+
# boundaries, not string boundaries, so a value with an embedded newline
|
|
54
|
+
# could smuggle a valid line past an otherwise-rejected string. [[ =~ ]]
|
|
55
|
+
# anchors to the whole string. The pattern must stay unquoted here —
|
|
56
|
+
# quoting the right-hand side of =~ forces literal string matching.
|
|
57
|
+
[[ "$r" =~ $pattern ]]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Validate a full candidate. Prints nothing on success; prints a reason slug
|
|
61
|
+
# to stderr on failure.
|
|
62
|
+
#
|
|
63
|
+
# Usage: librarian_lesson_validate_candidate <candidate_json>
|
|
64
|
+
librarian_lesson_validate_candidate() {
|
|
65
|
+
local candidate="${1:-}"
|
|
66
|
+
[[ -z "$candidate" ]] && { printf 'schema_invalid\n' >&2; return 1; }
|
|
67
|
+
|
|
68
|
+
# Structural shape, including the versioned-only rule and a non-empty
|
|
69
|
+
# resolution. `versions` must be a non-empty object. artifact_ids,
|
|
70
|
+
# session_ids, and observed_at are checked against the same patterns as
|
|
71
|
+
# the vendored lesson-evidence.subschema.json (ULID, non-empty string,
|
|
72
|
+
# RFC3339 date-time) — a provenance-less artifact (session_id/created_at
|
|
73
|
+
# stitched in as "") must fail here, not pass through and get buried
|
|
74
|
+
# permanently once librarian_lesson_seen marks it handled.
|
|
75
|
+
#
|
|
76
|
+
# The `keys - [...] | length == 0` checks mirror `additionalProperties:
|
|
77
|
+
# false` on the vendored `evidence` and `applies_to` sub-schemas
|
|
78
|
+
# (including the "versioned" scope branch), and the `all(type ==
|
|
79
|
+
# "string" and length > 0)` checks mirror their array items' `minLength:
|
|
80
|
+
# 1`. Neither is decorative: without them a model that "helpfully" adds
|
|
81
|
+
# an extra field, or emits an empty-string array entry, produces a
|
|
82
|
+
# proposal that passes here but fails ajv against the contract it claims
|
|
83
|
+
# to satisfy — and lessons are meant to be shared with other people. Each
|
|
84
|
+
# `keys` call is guarded by a preceding `type == "object"` check in the
|
|
85
|
+
# same `and` chain: jq's `and` short-circuits left to right, so `keys` on
|
|
86
|
+
# a missing/non-object value is never reached.
|
|
87
|
+
if ! printf '%s' "$candidate" | jq -e '
|
|
88
|
+
(.claim | type) == "string" and (.claim | length) > 0
|
|
89
|
+
and (.rationale | type) == "string" and (.rationale | length) > 0
|
|
90
|
+
and (.evidence | type) == "object"
|
|
91
|
+
and ((.evidence | keys) - ["artifact_ids", "session_ids", "project_key", "observed_at", "resolution"] | length) == 0
|
|
92
|
+
and (.evidence.artifact_ids | type) == "array" and (.evidence.artifact_ids | length) > 0
|
|
93
|
+
and (.evidence.artifact_ids | all(type == "string" and test("^[0-9A-HJKMNP-TV-Z]{26}$")))
|
|
94
|
+
and (.evidence.session_ids | type) == "array" and (.evidence.session_ids | length) > 0
|
|
95
|
+
and (.evidence.session_ids | all(type == "string" and length > 0))
|
|
96
|
+
and (.evidence.project_key | type) == "string"
|
|
97
|
+
and (.evidence.project_key | test("^[0-9a-f]{12}$"))
|
|
98
|
+
and (.evidence.observed_at | type) == "string"
|
|
99
|
+
and (.evidence.observed_at | test("^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"))
|
|
100
|
+
and (.evidence.resolution | type) == "string" and (.evidence.resolution | length) > 0
|
|
101
|
+
and (.applies_to | type) == "object"
|
|
102
|
+
and ((.applies_to | keys) - ["stack", "scope", "file_patterns", "task_kinds"] | length) == 0
|
|
103
|
+
and (.applies_to.stack | type) == "array" and (.applies_to.stack | length) > 0
|
|
104
|
+
and (.applies_to.stack | all(type == "string" and length > 0))
|
|
105
|
+
and (.applies_to.file_patterns | type) == "array"
|
|
106
|
+
and (.applies_to.file_patterns | all(type == "string" and length > 0))
|
|
107
|
+
and (.applies_to.task_kinds | type) == "array"
|
|
108
|
+
and (.applies_to.task_kinds | all(type == "string" and length > 0))
|
|
109
|
+
and .applies_to.scope.kind == "versioned"
|
|
110
|
+
and ((.applies_to.scope | keys) - ["kind", "versions"] | length) == 0
|
|
111
|
+
and (.applies_to.scope.versions | type) == "object"
|
|
112
|
+
and (.applies_to.scope.versions | length) > 0
|
|
113
|
+
' >/dev/null 2>&1; then
|
|
114
|
+
printf 'schema_invalid\n' >&2
|
|
115
|
+
return 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Cross-field rule JSON Schema cannot express: every versions key must
|
|
119
|
+
# name an entry in stack.
|
|
120
|
+
if ! printf '%s' "$candidate" | jq -e '
|
|
121
|
+
(.applies_to.scope.versions | keys) - .applies_to.stack | length == 0
|
|
122
|
+
' >/dev/null 2>&1; then
|
|
123
|
+
printf 'schema_invalid\n' >&2
|
|
124
|
+
return 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Every range must satisfy the vendored pattern. NUL-delimited, not
|
|
128
|
+
# newline-delimited: a range value with an embedded newline would
|
|
129
|
+
# otherwise split into two lines that can each pass individually even
|
|
130
|
+
# though the single value they came from is not a valid range. Do not
|
|
131
|
+
# skip empty reads either — jq never emits one for a non-empty object
|
|
132
|
+
# of strings, so an empty read means the range itself is empty, and
|
|
133
|
+
# librarian_lesson_valid_range already rejects that.
|
|
134
|
+
local range
|
|
135
|
+
while IFS= read -r -d '' range; do
|
|
136
|
+
librarian_lesson_valid_range "$range" || { printf 'schema_invalid\n' >&2; return 1; }
|
|
137
|
+
done < <(printf '%s' "$candidate" | jq --raw-output0 '.applies_to.scope.versions[]' 2>/dev/null)
|
|
138
|
+
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tribunal",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Multi-agent execution with LLM-as-a-Judge quality gates. An Actor performs work; a jury of typed Judges scores it against a project-overridable rubric; a Meta-Judge reviews the jury for bias; the gate decides accept, retry, or exhaust. Grounded in LLM-as-a-Judge (Zheng et al. 2023) and LLM-as-a-Meta-Judge (Wu et al. 2024). Builds on the Onlooker ecosystem plugin.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.3](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.1.2...tribunal-v1.1.3) (2026-08-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* make the local bats suite tell the truth :mag: ([#135](https://github.com/onlooker-community/ecosystem/issues/135)) ([f0763e0](https://github.com/onlooker-community/ecosystem/commit/f0763e09f3caf2d39c89f28befd12567af0af845))
|
|
9
|
+
|
|
3
10
|
## [1.1.2](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.1.1...tribunal-v1.1.2) (2026-08-02)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
tribunal_aggregate() {
|
|
26
26
|
local method="${1:-mean}"
|
|
27
27
|
local verdicts="${2:-[]}"
|
|
28
|
-
|
|
28
|
+
# reserved for true weighted_mean once per-criterion scores are threaded
|
|
29
|
+
local _rubric="${3:-}"
|
|
30
|
+
[ -z "$_rubric" ] && _rubric='{}'
|
|
29
31
|
: "$_rubric"
|
|
30
32
|
|
|
31
33
|
local count
|
|
@@ -23,7 +23,8 @@ tribunal_gate_decide() {
|
|
|
23
23
|
local verdicts="${2:-[]}"
|
|
24
24
|
local aggregated_score="${3:-0}"
|
|
25
25
|
local score_threshold="${4:-0.75}"
|
|
26
|
-
local meta="${5:-
|
|
26
|
+
local meta="${5:-}"
|
|
27
|
+
[ -z "$meta" ] && meta='{}'
|
|
27
28
|
local dissent_score="${6:-0}"
|
|
28
29
|
local dissent_threshold="${7:-0.25}"
|
|
29
30
|
|
|
@@ -148,7 +148,12 @@ prompt_rules_pattern_matches() {
|
|
|
148
148
|
prompt_rules_emit() {
|
|
149
149
|
local session_id="${1:-unknown}"
|
|
150
150
|
local event_type="${2:-}"
|
|
151
|
-
|
|
151
|
+
# Default the payload without inlining braces in the expansion. Both
|
|
152
|
+
# `${3:-{\}}` and `${3:-{}}` are wrong: the first keeps the backslash on
|
|
153
|
+
# bash 3.2 (macOS system bash), and the second appends a stray `}` to any
|
|
154
|
+
# payload that IS supplied, on every bash version.
|
|
155
|
+
local payload_json="${3:-}"
|
|
156
|
+
[ -z "$payload_json" ] && payload_json='{}'
|
|
152
157
|
[[ -z "$event_type" ]] && return 1
|
|
153
158
|
ensure_file_exists "$ONLOOKER_EVENTS_LOG" || return 1
|
|
154
159
|
|