@onlooker-community/ecosystem 0.34.0 → 0.43.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/skills/writing-tests/SKILL.md +27 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/release.yml +14 -14
- package/.release-please-manifest.json +7 -7
- package/AGENTS.md +116 -0
- package/CHANGELOG.md +175 -0
- package/CLAUDE.md +5 -0
- package/docs/lesson-promotion-pipeline.md +6 -6
- package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
- package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
- package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
- package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
- package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
- package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
- package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
- package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
- package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
- package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
- package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
- package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
- package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
- package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
- package/package.json +2 -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/cartographer/.claude-plugin/plugin.json +1 -1
- package/plugins/cartographer/CHANGELOG.md +54 -0
- package/plugins/cartographer/README.md +56 -1
- package/plugins/cartographer/config.json +7 -1
- package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
- package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
- package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
- package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
- package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
- package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
- package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
- package/plugins/cartographer/scripts/run-audit.sh +136 -53
- package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
- 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 +71 -0
- package/plugins/librarian/config.json +30 -1
- package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
- package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
- package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
- package/plugins/librarian/scripts/lib/librarian-cli.sh +346 -2
- package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
- package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
- package/plugins/librarian/skills/librarian/SKILL.md +98 -1
- package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
- package/plugins/tribunal/CHANGELOG.md +63 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
- package/plugins/tribunal/config.json +1 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -9
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +195 -16
- package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
- package/scripts/lib/prompt-rules.sh +6 -1
- 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-config.bats +43 -0
- package/test/bats/cartographer-events.bats +267 -0
- package/test/bats/cartographer-filter.bats +169 -0
- package/test/bats/cartographer-lock.bats +3 -3
- package/test/bats/cartographer-omission.bats +202 -0
- package/test/bats/cartographer-resolve.bats +287 -0
- package/test/bats/cartographer-run-audit.bats +325 -0
- 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-author-key.bats +477 -0
- package/test/bats/librarian-cli.bats +16 -16
- package/test/bats/librarian-lesson-judge.bats +884 -0
- package/test/bats/librarian-lesson-promote.bats +552 -0
- package/test/bats/librarian-lesson-review.bats +900 -0
- package/test/bats/librarian-lesson-transform.bats +17 -3
- package/test/bats/librarian-session-end.bats +161 -0
- package/test/bats/librarian-session-start.bats +36 -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-aggregate.bats +354 -1
- package/test/bats/tribunal-gate.bats +631 -0
- package/test/bats/tribunal-judge-agents.bats +119 -0
- 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-validate-agreement.test.mjs +35 -6
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Lesson confirmation — the human intent filter.
|
|
3
|
+
#
|
|
4
|
+
# The human picks which candidates go to the jury and their visibility, before
|
|
5
|
+
# any expensive tokens are spent. Intent is cheap and only a human can supply
|
|
6
|
+
# it; quality is expensive and only the jury can judge it. Splitting them here
|
|
7
|
+
# means cost scales with intent rather than artifact volume.
|
|
8
|
+
#
|
|
9
|
+
# NOTHING in this file may invoke a model. That is what makes "no tokens spent
|
|
10
|
+
# on unselected candidates" a property of the code rather than a promise.
|
|
11
|
+
#
|
|
12
|
+
# Requires librarian-lesson-storage.sh and librarian-lesson-validate.sh.
|
|
13
|
+
|
|
14
|
+
librarian_lesson_passed_path() {
|
|
15
|
+
local key="$1"
|
|
16
|
+
printf '%s/passed.jsonl' "$(librarian_lessons_dir "$key")"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Print proposals in the given status as a JSON array, oldest first. Prints []
|
|
20
|
+
# when none.
|
|
21
|
+
#
|
|
22
|
+
# A malformed or racing file must shrink the result, never erase it: a
|
|
23
|
+
# truncated write, a bare-value file, or a confirm/pass rewriting the file
|
|
24
|
+
# mid-read all read as "that entry is missing" here, not as an error that
|
|
25
|
+
# discards every proposal already gathered. `select(type == "object" and
|
|
26
|
+
# ...)` folds the parse guard and the status check into a single read of
|
|
27
|
+
# each file, the same shape librarian_lesson_seen uses for the same reason.
|
|
28
|
+
#
|
|
29
|
+
# Usage: librarian_lesson_list_by_status <key> <status>
|
|
30
|
+
librarian_lesson_list_by_status() {
|
|
31
|
+
local key="$1"
|
|
32
|
+
local status="$2"
|
|
33
|
+
[[ -z "$key" || -z "$status" ]] && { printf '[]'; return 0; }
|
|
34
|
+
|
|
35
|
+
local dir
|
|
36
|
+
dir="$(librarian_lessons_dir "$key")/proposals"
|
|
37
|
+
[[ -d "$dir" ]] || { printf '[]'; return 0; }
|
|
38
|
+
|
|
39
|
+
local f out entry merged
|
|
40
|
+
out='[]'
|
|
41
|
+
for f in "$dir"/*.json; do
|
|
42
|
+
[[ -f "$f" ]] || continue
|
|
43
|
+
entry=$(jq -c --arg s "$status" 'select(type == "object" and .status == $s)' "$f" 2>/dev/null)
|
|
44
|
+
[[ -z "$entry" ]] && continue
|
|
45
|
+
# Merge into a separate variable, not directly into $out: `out=$(cmd)
|
|
46
|
+
# || continue` still assigns $out to cmd's (possibly empty) stdout
|
|
47
|
+
# before the `||` is ever evaluated, so a failed merge would silently
|
|
48
|
+
# wipe everything already gathered even with `continue` guarding it.
|
|
49
|
+
merged=$(jq -c --argjson e "$entry" '. + [$e]' <<<"$out" 2>/dev/null) || continue
|
|
50
|
+
out="$merged"
|
|
51
|
+
done
|
|
52
|
+
printf '%s' "$(jq -c 'sort_by(.created_at)' <<<"$out")"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Pending is the queue the review walk drives, so it keeps its own name.
|
|
56
|
+
#
|
|
57
|
+
# Usage: librarian_lesson_list_pending <key>
|
|
58
|
+
librarian_lesson_list_pending() {
|
|
59
|
+
librarian_lesson_list_by_status "${1:-}" "pending"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_librarian_lesson_valid_visibility() {
|
|
63
|
+
case "${1:-}" in
|
|
64
|
+
private | org | public) return 0 ;;
|
|
65
|
+
*) return 1 ;;
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Confirm a candidate for the jury.
|
|
70
|
+
#
|
|
71
|
+
# Usage: librarian_lesson_confirm <key> <lesson_id> <visibility> [justification]
|
|
72
|
+
#
|
|
73
|
+
# With a justification, the candidate's scope is rewritten to
|
|
74
|
+
# version_independent. That branch is refused at private visibility: private
|
|
75
|
+
# lessons run no jury, so the justification would reach the pool with nothing
|
|
76
|
+
# checking it — the same hole the transform closes by refusing the branch
|
|
77
|
+
# outright. Requiring org or public means scope_accuracy actually tests it.
|
|
78
|
+
librarian_lesson_confirm() {
|
|
79
|
+
local key="$1"
|
|
80
|
+
local lesson_id="$2"
|
|
81
|
+
local visibility="${3:-}"
|
|
82
|
+
local justification="${4:-}"
|
|
83
|
+
[[ -z "$key" || -z "$lesson_id" ]] && return 1
|
|
84
|
+
|
|
85
|
+
_librarian_lesson_valid_visibility "$visibility" || {
|
|
86
|
+
printf 'visibility must be one of: private, org, public\n' >&2
|
|
87
|
+
return 1
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if [[ -n "$justification" && "$visibility" == "private" ]]; then
|
|
91
|
+
printf 'version_independent requires org or public visibility: a private lesson runs no jury, so its justification would go unchecked and the lesson would never expire\n' >&2
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
local path
|
|
96
|
+
path="$(librarian_lessons_dir "$key")/proposals/${lesson_id}.json"
|
|
97
|
+
[[ -f "$path" ]] || { printf 'Lesson %s not found.\n' "$lesson_id" >&2; return 1; }
|
|
98
|
+
|
|
99
|
+
local proposal candidate current_status
|
|
100
|
+
proposal=$(jq '.' "$path" 2>/dev/null) || return 1
|
|
101
|
+
candidate=$(printf '%s' "$proposal" | jq -c '.candidate' 2>/dev/null) || return 1
|
|
102
|
+
current_status=$(printf '%s' "$proposal" | jq -r '.status // ""' 2>/dev/null)
|
|
103
|
+
|
|
104
|
+
# Snapshot the candidate before the rewrite, so unconfirm can put it back.
|
|
105
|
+
# Only when we actually rewrite: a plain confirm never touches .candidate,
|
|
106
|
+
# so a snapshot there would be dead weight that unconfirm has to reason
|
|
107
|
+
# about. Absence of the field means "nothing was mutated".
|
|
108
|
+
local candidate_before=""
|
|
109
|
+
if [[ -n "$justification" ]]; then
|
|
110
|
+
candidate_before="$candidate"
|
|
111
|
+
candidate=$(printf '%s' "$candidate" | jq -c \
|
|
112
|
+
--arg j "$justification" \
|
|
113
|
+
'.applies_to.scope = {kind: "version_independent", justification: $j}' 2>/dev/null) || return 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Authoritative guard: check the RESULTING state, not just this call's
|
|
117
|
+
# input. The check above only catches a justification supplied on THIS
|
|
118
|
+
# invocation; a candidate that already carries version_independent scope
|
|
119
|
+
# (proposed that way, or confirmed non-private earlier and now being
|
|
120
|
+
# reconfirmed at private) would sail past it and write with no jury ever
|
|
121
|
+
# validating the justification. This check is the one that actually holds
|
|
122
|
+
# the invariant — the input check above is only a fast path that avoids
|
|
123
|
+
# reading the file for the common case.
|
|
124
|
+
if [[ "$visibility" == "private" ]] && printf '%s' "$candidate" \
|
|
125
|
+
| jq -e '.applies_to.scope.kind == "version_independent"' >/dev/null 2>&1; then
|
|
126
|
+
printf 'version_independent requires org or public visibility: a private lesson runs no jury, so its justification would go unchecked and the lesson would never expire\n' >&2
|
|
127
|
+
return 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Guard the transition, not just the write. Each write below is already
|
|
131
|
+
# atomic (one merged jq expression, one write), but an unguarded SEQUENCE
|
|
132
|
+
# is where a passed candidate could be flipped back to confirmed while
|
|
133
|
+
# passed.jsonl still asserts the human declined it — the durable record
|
|
134
|
+
# of intent disagreeing with the candidate it describes.
|
|
135
|
+
case "$current_status" in
|
|
136
|
+
pending) ;;
|
|
137
|
+
confirmed)
|
|
138
|
+
# Idempotent only when the repeat asks for exactly what is
|
|
139
|
+
# already recorded; anything else is a contradictory flip.
|
|
140
|
+
if printf '%s' "$proposal" | jq -e \
|
|
141
|
+
--arg v "$visibility" --argjson c "$candidate" \
|
|
142
|
+
'.visibility == $v and .candidate == $c' >/dev/null 2>&1; then
|
|
143
|
+
return 0
|
|
144
|
+
fi
|
|
145
|
+
printf 'Lesson %s is already confirmed; visibility or scope differs from the request.\n' \
|
|
146
|
+
"$lesson_id" >&2
|
|
147
|
+
return 1
|
|
148
|
+
;;
|
|
149
|
+
passed)
|
|
150
|
+
printf 'Lesson %s was passed on; it cannot be confirmed.\n' "$lesson_id" >&2
|
|
151
|
+
return 1
|
|
152
|
+
;;
|
|
153
|
+
*)
|
|
154
|
+
printf 'Lesson %s has an unrecognized status: %s\n' "$lesson_id" "$current_status" >&2
|
|
155
|
+
return 1
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
|
|
159
|
+
librarian_lesson_validate_confirmed "$candidate" 2>/dev/null || {
|
|
160
|
+
printf 'Candidate does not validate; not confirmed.\n' >&2
|
|
161
|
+
return 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
local now updated
|
|
165
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
166
|
+
# `.candidate` is assigned, not merged into with `*`: `*` is a recursive
|
|
167
|
+
# merge, and merging the fresh candidate over the stored one leaves stale
|
|
168
|
+
# keys behind — a version_independent rewrite would keep the old
|
|
169
|
+
# versioned scope's `versions` key alongside the new `justification`,
|
|
170
|
+
# producing a candidate that fails its own validator's
|
|
171
|
+
# additionalProperties check on the way out the door.
|
|
172
|
+
if [[ -n "$candidate_before" ]]; then
|
|
173
|
+
updated=$(printf '%s' "$proposal" | jq \
|
|
174
|
+
--arg v "$visibility" --arg t "$now" \
|
|
175
|
+
--argjson c "$candidate" --argjson cb "$candidate_before" \
|
|
176
|
+
'. * {status: "confirmed", visibility: $v, confirmed_at: $t}
|
|
177
|
+
| .candidate = $c
|
|
178
|
+
| .candidate_before_confirm = $cb' 2>/dev/null) || return 1
|
|
179
|
+
else
|
|
180
|
+
# No rewrite happened, so there is nothing to snapshot — and any field
|
|
181
|
+
# already on the proposal is stale by definition. Deleting it holds the
|
|
182
|
+
# invariant unconfirm relies on ("absence means the candidate was never
|
|
183
|
+
# mutated") from both ends: without this, a proposal that arrives
|
|
184
|
+
# carrying the field keeps it through a plain confirm, and the next
|
|
185
|
+
# unconfirm silently replaces .candidate with a snapshot describing a
|
|
186
|
+
# rewrite this confirm never performed.
|
|
187
|
+
updated=$(printf '%s' "$proposal" | jq \
|
|
188
|
+
--arg v "$visibility" --arg t "$now" --argjson c "$candidate" \
|
|
189
|
+
'. * {status: "confirmed", visibility: $v, confirmed_at: $t}
|
|
190
|
+
| .candidate = $c
|
|
191
|
+
| del(.candidate_before_confirm)' 2>/dev/null) || return 1
|
|
192
|
+
fi
|
|
193
|
+
[[ -z "$updated" || "$updated" == "null" ]] && return 1
|
|
194
|
+
librarian_lesson_write_atomic "$path" "$updated"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Take back a confirmation, before the jury has seen the lesson.
|
|
198
|
+
#
|
|
199
|
+
# `confirmed` is otherwise terminal: confirm refuses a differing repeat and
|
|
200
|
+
# pass refuses a confirmed lesson. Those guards are correct — they are what
|
|
201
|
+
# keeps passed.jsonl from contradicting the proposal it describes — but they
|
|
202
|
+
# left no way back from confirming at the wrong visibility, and `public` is
|
|
203
|
+
# the tier that leaves this machine.
|
|
204
|
+
#
|
|
205
|
+
# Proceeds ONLY from `confirmed`. Every other status is refused, which is also
|
|
206
|
+
# what makes this forward-safe: when the jury stage introduces a status of its
|
|
207
|
+
# own, this verb refuses it through the catch-all with no change here.
|
|
208
|
+
#
|
|
209
|
+
# Usage: librarian_lesson_unconfirm <key> <lesson_id>
|
|
210
|
+
librarian_lesson_unconfirm() {
|
|
211
|
+
local key="$1"
|
|
212
|
+
local lesson_id="$2"
|
|
213
|
+
[[ -z "$key" || -z "$lesson_id" ]] && return 1
|
|
214
|
+
|
|
215
|
+
local path
|
|
216
|
+
path="$(librarian_lessons_dir "$key")/proposals/${lesson_id}.json"
|
|
217
|
+
[[ -f "$path" ]] || { printf 'Lesson %s not found.\n' "$lesson_id" >&2; return 1; }
|
|
218
|
+
|
|
219
|
+
local current_status
|
|
220
|
+
current_status=$(jq -r '.status // ""' "$path" 2>/dev/null)
|
|
221
|
+
|
|
222
|
+
case "$current_status" in
|
|
223
|
+
confirmed) ;;
|
|
224
|
+
pending) return 0 ;;
|
|
225
|
+
passed)
|
|
226
|
+
# Passing is a different decision with its own durable record.
|
|
227
|
+
# Silently moving it back to pending would leave passed.jsonl
|
|
228
|
+
# asserting a decision the proposal contradicts.
|
|
229
|
+
printf 'Lesson %s was passed on, not confirmed; unconfirm does not undo that.\n' "$lesson_id" >&2
|
|
230
|
+
return 1
|
|
231
|
+
;;
|
|
232
|
+
*)
|
|
233
|
+
printf 'Lesson %s has an unrecognized status: %s\n' "$lesson_id" "$current_status" >&2
|
|
234
|
+
return 1
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
|
|
238
|
+
# Restore the pre-confirm candidate when one was snapshotted, and delete
|
|
239
|
+
# the snapshot either way. A stale snapshot left on a pending proposal is
|
|
240
|
+
# indistinguishable from a live one at the next confirm, and would
|
|
241
|
+
# silently revert a later legitimate rewrite.
|
|
242
|
+
local updated
|
|
243
|
+
updated=$(jq '
|
|
244
|
+
(if has("candidate_before_confirm") then .candidate = .candidate_before_confirm else . end)
|
|
245
|
+
| del(.candidate_before_confirm, .visibility, .confirmed_at)
|
|
246
|
+
| .status = "pending"
|
|
247
|
+
' "$path" 2>/dev/null) || return 1
|
|
248
|
+
[[ -z "$updated" || "$updated" == "null" ]] && return 1
|
|
249
|
+
librarian_lesson_write_atomic "$path" "$updated"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Decline to share a candidate.
|
|
253
|
+
#
|
|
254
|
+
# The file is KEPT. librarian_lesson_seen scans proposals/ by artifact_id, so
|
|
255
|
+
# leaving it in place is what stops the artifact being re-proposed on the next
|
|
256
|
+
# scan and re-paying for a transform whose answer the human already gave.
|
|
257
|
+
#
|
|
258
|
+
# The ledger is separate from declined.jsonl on purpose: that file records
|
|
259
|
+
# machine verdicts and feeds rubric tuning, and folding human intent into it
|
|
260
|
+
# would corrupt the signal it exists to carry.
|
|
261
|
+
#
|
|
262
|
+
# Usage: librarian_lesson_pass <key> <lesson_id> [reason]
|
|
263
|
+
librarian_lesson_pass() {
|
|
264
|
+
local key="$1"
|
|
265
|
+
local lesson_id="$2"
|
|
266
|
+
local reason="${3:-}"
|
|
267
|
+
[[ -z "$key" || -z "$lesson_id" ]] && return 1
|
|
268
|
+
|
|
269
|
+
local path
|
|
270
|
+
path="$(librarian_lessons_dir "$key")/proposals/${lesson_id}.json"
|
|
271
|
+
[[ -f "$path" ]] || { printf 'Lesson %s not found.\n' "$lesson_id" >&2; return 1; }
|
|
272
|
+
|
|
273
|
+
local current_status
|
|
274
|
+
current_status=$(jq -r '.status // ""' "$path" 2>/dev/null)
|
|
275
|
+
|
|
276
|
+
# Same reasoning as confirm: guard the transition. Passing twice must not
|
|
277
|
+
# append a second ledger line, and a confirmed candidate must not be
|
|
278
|
+
# silently un-confirmed — that would leave stale visibility/confirmed_at
|
|
279
|
+
# behind and send a contradictory signal to the stages that select on
|
|
280
|
+
# status downstream.
|
|
281
|
+
case "$current_status" in
|
|
282
|
+
pending) ;;
|
|
283
|
+
passed) return 0 ;;
|
|
284
|
+
confirmed)
|
|
285
|
+
printf 'Lesson %s is confirmed for the jury; it cannot be passed on now.\n' "$lesson_id" >&2
|
|
286
|
+
return 1
|
|
287
|
+
;;
|
|
288
|
+
*)
|
|
289
|
+
printf 'Lesson %s has an unrecognized status: %s\n' "$lesson_id" "$current_status" >&2
|
|
290
|
+
return 1
|
|
291
|
+
;;
|
|
292
|
+
esac
|
|
293
|
+
|
|
294
|
+
local artifact_id now updated
|
|
295
|
+
artifact_id=$(jq -r '.artifact_id // ""' "$path" 2>/dev/null)
|
|
296
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
297
|
+
|
|
298
|
+
updated=$(jq --arg t "$now" '. * {status: "passed", passed_at: $t}' "$path" 2>/dev/null) || return 1
|
|
299
|
+
[[ -z "$updated" || "$updated" == "null" ]] && return 1
|
|
300
|
+
librarian_lesson_write_atomic "$path" "$updated" || return 1
|
|
301
|
+
|
|
302
|
+
local line
|
|
303
|
+
line=$(jq -cn \
|
|
304
|
+
--arg lesson_id "$lesson_id" \
|
|
305
|
+
--arg artifact_id "$artifact_id" \
|
|
306
|
+
--arg reason "$reason" \
|
|
307
|
+
--arg at "$now" \
|
|
308
|
+
'{lesson_id: $lesson_id, artifact_id: $artifact_id,
|
|
309
|
+
reason: (if $reason == "" then null else $reason end), passed_at: $at}') || return 1
|
|
310
|
+
|
|
311
|
+
printf '%s\n' "$line" >> "$(librarian_lesson_passed_path "$key")"
|
|
312
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Rubric selection for lesson judging.
|
|
3
|
+
#
|
|
4
|
+
# Two builtins live in config.json under librarian.lesson_judging.rubrics,
|
|
5
|
+
# mirroring tribunal's rubric.builtins shape so they stay legible to anyone who
|
|
6
|
+
# knows tribunal. Librarian loads them itself rather than sourcing tribunal's
|
|
7
|
+
# lib — see docs/adr/002-agent-definitions-are-shared-assets.md.
|
|
8
|
+
#
|
|
9
|
+
# The per-criterion weights and min_pass floors are LIVE as of ecosystem-pht:
|
|
10
|
+
# librarian_lesson_aggregate weights them and librarian_lesson_gate blocks on
|
|
11
|
+
# any criterion below its floor.
|
|
12
|
+
#
|
|
13
|
+
# `disclosure` at min_pass 0.9 is what makes the public tier stricter than org.
|
|
14
|
+
# It replaces gate_policy `unanimous`, which was intended as a stand-in for
|
|
15
|
+
# exactly this and turned out to be a no-op: at the configured two-judge panel,
|
|
16
|
+
# `unanimous` and `majority` agree on every possible pass count. See
|
|
17
|
+
# ecosystem-j74. Changing judge_types without re-reading that bead is how the
|
|
18
|
+
# hole reopens.
|
|
19
|
+
#
|
|
20
|
+
# Exposes:
|
|
21
|
+
# librarian_lesson_rubric_id_for_visibility <visibility>
|
|
22
|
+
# librarian_lesson_rubric_get <rubric_id>
|
|
23
|
+
|
|
24
|
+
# Map a confirmed lesson's visibility to the rubric that judges it.
|
|
25
|
+
#
|
|
26
|
+
# `private` maps to the empty string on purpose: that tier runs no jury at all,
|
|
27
|
+
# which is what makes cost scale with intent rather than artifact volume.
|
|
28
|
+
#
|
|
29
|
+
# Usage: librarian_lesson_rubric_id_for_visibility <visibility>
|
|
30
|
+
librarian_lesson_rubric_id_for_visibility() {
|
|
31
|
+
case "${1:-}" in
|
|
32
|
+
private) printf '' ;;
|
|
33
|
+
org) printf 'lesson-promotion' ;;
|
|
34
|
+
public) printf 'lesson-promotion-public' ;;
|
|
35
|
+
*) return 1 ;;
|
|
36
|
+
esac
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Echo one rubric as compact JSON. Returns 1 and echoes nothing if unknown.
|
|
41
|
+
#
|
|
42
|
+
# Usage: librarian_lesson_rubric_get <rubric_id>
|
|
43
|
+
librarian_lesson_rubric_get() {
|
|
44
|
+
local rubric_id="${1:-}"
|
|
45
|
+
[[ -z "$rubric_id" ]] && return 1
|
|
46
|
+
|
|
47
|
+
local rubrics found
|
|
48
|
+
rubrics=$(librarian_config_get '.librarian.lesson_judging.rubrics')
|
|
49
|
+
[[ -z "$rubrics" || "$rubrics" == "null" ]] && return 1
|
|
50
|
+
|
|
51
|
+
found=$(printf '%s' "$rubrics" | jq -c --arg id "$rubric_id" \
|
|
52
|
+
'map(select(.id == $id)) | first // empty' 2>/dev/null) || return 1
|
|
53
|
+
[[ -z "$found" || "$found" == "null" ]] && return 1
|
|
54
|
+
|
|
55
|
+
printf '%s' "$found"
|
|
56
|
+
}
|
|
@@ -25,6 +25,23 @@ librarian_lesson_storage_init() {
|
|
|
25
25
|
mkdir -p "$dir/proposals" "$dir/approved" 2>/dev/null
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
# Write a file atomically: temp in the same directory, then mv.
|
|
29
|
+
#
|
|
30
|
+
# `printf > "$path"` truncates before writing, so an interrupted write leaves
|
|
31
|
+
# a zero-byte file. For a proposal that is permanently stuck: every verb
|
|
32
|
+
# refuses it with "unrecognized status: " and list_pending hides it, so even
|
|
33
|
+
# unconfirm — the recovery verb — cannot bring it back.
|
|
34
|
+
#
|
|
35
|
+
# Usage: librarian_lesson_write_atomic <path> <content>
|
|
36
|
+
librarian_lesson_write_atomic() {
|
|
37
|
+
local path="$1"
|
|
38
|
+
local content="$2"
|
|
39
|
+
local tmp
|
|
40
|
+
tmp=$(mktemp "${path}.XXXXXX" 2>/dev/null) || return 1
|
|
41
|
+
printf '%s\n' "$content" > "$tmp" 2>/dev/null || { rm -f "$tmp" 2>/dev/null; return 1; }
|
|
42
|
+
mv "$tmp" "$path" 2>/dev/null || { rm -f "$tmp" 2>/dev/null; return 1; }
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
# Write one candidate. Prints the ULID on success.
|
|
29
46
|
# Usage: librarian_lesson_write_proposal <key> <candidate_json> <artifact_id>
|
|
30
47
|
librarian_lesson_write_proposal() {
|
|
@@ -61,29 +78,51 @@ librarian_lesson_write_proposal() {
|
|
|
61
78
|
# bury a good artifact permanently, because the watermark has already moved
|
|
62
79
|
# past it and declined entries are never re-read.
|
|
63
80
|
#
|
|
64
|
-
#
|
|
81
|
+
# `verdict` is emitted with --argjson so it lands as a nested object, not a
|
|
82
|
+
# serialized string: consumers read .verdict.judges[].score directly. Rows
|
|
83
|
+
# written by the transform have no verdict and simply lack the key — a format
|
|
84
|
+
# failure has no jury.
|
|
85
|
+
#
|
|
86
|
+
# Usage: librarian_lesson_append_declined <key> <artifact_id> <reason> [detail] [verdict_json]
|
|
65
87
|
librarian_lesson_append_declined() {
|
|
66
88
|
local key="$1"
|
|
67
89
|
local artifact_id="$2"
|
|
68
90
|
local reason="$3"
|
|
69
91
|
local detail="${4:-}"
|
|
92
|
+
local verdict="${5:-}"
|
|
70
93
|
[[ -z "$key" || -z "$artifact_id" || -z "$reason" ]] && return 1
|
|
71
94
|
|
|
72
95
|
librarian_lesson_storage_init "$key" || return 1
|
|
73
96
|
|
|
74
97
|
local now line
|
|
75
98
|
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
if [[ -n "$verdict" ]]; then
|
|
100
|
+
line=$(jq -cn \
|
|
101
|
+
--arg artifact_id "$artifact_id" \
|
|
102
|
+
--arg reason "$reason" \
|
|
103
|
+
--arg detail "$detail" \
|
|
104
|
+
--arg at "$now" \
|
|
105
|
+
--argjson verdict "$verdict" \
|
|
106
|
+
'{
|
|
107
|
+
artifact_id: $artifact_id,
|
|
108
|
+
reason: $reason,
|
|
109
|
+
detail: (if $detail == "" then null else $detail end),
|
|
110
|
+
declined_at: $at,
|
|
111
|
+
verdict: $verdict
|
|
112
|
+
}') || return 1
|
|
113
|
+
else
|
|
114
|
+
line=$(jq -cn \
|
|
115
|
+
--arg artifact_id "$artifact_id" \
|
|
116
|
+
--arg reason "$reason" \
|
|
117
|
+
--arg detail "$detail" \
|
|
118
|
+
--arg at "$now" \
|
|
119
|
+
'{
|
|
120
|
+
artifact_id: $artifact_id,
|
|
121
|
+
reason: $reason,
|
|
122
|
+
detail: (if $detail == "" then null else $detail end),
|
|
123
|
+
declined_at: $at
|
|
124
|
+
}') || return 1
|
|
125
|
+
fi
|
|
87
126
|
|
|
88
127
|
printf '%s\n' "$line" >> "$(librarian_lessons_dir "$key")/declined.jsonl"
|
|
89
128
|
}
|
|
@@ -123,8 +162,38 @@ librarian_lesson_seen() {
|
|
|
123
162
|
return 0
|
|
124
163
|
fi
|
|
125
164
|
|
|
165
|
+
# ===================================================================
|
|
166
|
+
# proposals/ IS THE SOLE DEDUP SOURCE FOR AN APPROVED LESSON.
|
|
167
|
+
# PRUNING proposals/ SILENTLY BREAKS DEDUP. See ecosystem-d0m.
|
|
168
|
+
# ===================================================================
|
|
169
|
+
#
|
|
170
|
+
# A declined artifact is covered by declined.jsonl above — a terminal
|
|
171
|
+
# record keyed by artifact_id that outlives its proposal. An APPROVED one
|
|
172
|
+
# has no equivalent: the pool entry is ZLesson, a strictObject whose key
|
|
173
|
+
# set has no artifact_id and cannot gain one (an extra key fails ingest —
|
|
174
|
+
# see librarian-lesson-promote.sh's key-set comment). So nothing promote
|
|
175
|
+
# writes can answer "was this artifact already handled?"
|
|
176
|
+
#
|
|
177
|
+
# This loop used to scan approved/*.json too. That branch could never
|
|
178
|
+
# match, and the test covering it fabricated an {artifact_id: ...} file
|
|
179
|
+
# promote cannot produce — dead code with a green test in front of it,
|
|
180
|
+
# which is why it survived. Dropped rather than left as a decorative
|
|
181
|
+
# safety net that reads like coverage.
|
|
182
|
+
#
|
|
183
|
+
# The consequence, accepted deliberately: prune a promoted lesson's
|
|
184
|
+
# proposal and its artifact reads as unseen, so it is re-transformed
|
|
185
|
+
# (Haiku), re-confirmed BY THE HUMAN AGAIN, and re-judged (Opus),
|
|
186
|
+
# producing a duplicate pool entry for a lesson already promoted.
|
|
187
|
+
#
|
|
188
|
+
# The alternative was an artifact_id -> lesson id index (or a per-entry
|
|
189
|
+
# sidecar) to make the pool self-identifying. Neither buys anything until
|
|
190
|
+
# someone actually wants to prune, and both add a surface that can drift
|
|
191
|
+
# from the directory — so the requirement is stated instead of engineered
|
|
192
|
+
# around. Build the index FIRST if you ever need to prune; the tripwire
|
|
193
|
+
# test is "an approved lesson is NOT seen once its proposal is gone" in
|
|
194
|
+
# test/bats/librarian-lesson-promote.bats.
|
|
126
195
|
local f
|
|
127
|
-
for f in "$dir"/proposals/*.json
|
|
196
|
+
for f in "$dir"/proposals/*.json; do
|
|
128
197
|
[[ -f "$f" ]] || continue
|
|
129
198
|
if jq -e --arg a "$artifact_id" '.artifact_id == $a' "$f" >/dev/null 2>&1; then
|
|
130
199
|
return 0
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
#
|
|
13
13
|
# Config inputs (read via librarian_config_get from librarian_lesson_call):
|
|
14
14
|
# librarian.lesson_transform.model Anthropic model id
|
|
15
|
-
# librarian.lesson_transform.timeout_seconds
|
|
15
|
+
# librarian.lesson_transform.timeout_seconds Per-call wall-clock ceiling
|
|
16
|
+
#
|
|
17
|
+
# timeout_seconds bounds one call, not a scan. The aggregate ceiling across a
|
|
18
|
+
# whole SessionEnd is librarian.lesson_transform.total_budget_ms, enforced by
|
|
19
|
+
# the caller in librarian-session-end.sh — a per-call timeout alone leaves
|
|
20
|
+
# KEPT_COUNT of them unbounded end to end (ecosystem-qwi).
|
|
16
21
|
|
|
17
22
|
# Fallback when config hasn't been loaded or leaves the key unset.
|
|
18
23
|
_LIBRARIAN_LESSON_DEFAULT_TIMEOUT_SECONDS=20
|