@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.
Files changed (112) hide show
  1. package/.claude/skills/writing-tests/SKILL.md +27 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/release.yml +14 -14
  4. package/.release-please-manifest.json +7 -7
  5. package/AGENTS.md +116 -0
  6. package/CHANGELOG.md +175 -0
  7. package/CLAUDE.md +5 -0
  8. package/docs/lesson-promotion-pipeline.md +6 -6
  9. package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
  10. package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
  11. package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
  12. package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
  13. package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
  14. package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
  15. package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
  16. package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
  17. package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
  18. package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
  19. package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
  20. package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
  21. package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
  22. package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
  23. package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
  24. package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
  25. package/package.json +2 -2
  26. package/plugins/assayer/.claude-plugin/plugin.json +1 -1
  27. package/plugins/assayer/CHANGELOG.md +7 -0
  28. package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
  29. package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
  30. package/plugins/cartographer/CHANGELOG.md +54 -0
  31. package/plugins/cartographer/README.md +56 -1
  32. package/plugins/cartographer/config.json +7 -1
  33. package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
  34. package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
  35. package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
  36. package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
  37. package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
  38. package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
  39. package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
  40. package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
  41. package/plugins/cartographer/scripts/run-audit.sh +136 -53
  42. package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
  43. package/plugins/curator/.claude-plugin/plugin.json +1 -1
  44. package/plugins/curator/CHANGELOG.md +7 -0
  45. package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
  46. package/plugins/historian/.claude-plugin/plugin.json +1 -1
  47. package/plugins/historian/CHANGELOG.md +7 -0
  48. package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
  49. package/plugins/librarian/.claude-plugin/plugin.json +1 -1
  50. package/plugins/librarian/CHANGELOG.md +71 -0
  51. package/plugins/librarian/config.json +30 -1
  52. package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
  53. package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
  54. package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
  55. package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
  56. package/plugins/librarian/scripts/lib/librarian-cli.sh +346 -2
  57. package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
  58. package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
  59. package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
  60. package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
  61. package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
  62. package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
  63. package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
  64. package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
  65. package/plugins/librarian/skills/librarian/SKILL.md +98 -1
  66. package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
  67. package/plugins/tribunal/CHANGELOG.md +63 -0
  68. package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
  69. package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
  70. package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
  71. package/plugins/tribunal/config.json +1 -0
  72. package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -9
  73. package/plugins/tribunal/scripts/lib/tribunal-gate.sh +195 -16
  74. package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
  75. package/scripts/lib/prompt-rules.sh +6 -1
  76. package/test/bats/archivist-inject.bats +1 -1
  77. package/test/bats/assayer-extract.bats +2 -2
  78. package/test/bats/bursar-session-start.bats +3 -3
  79. package/test/bats/cartographer-config.bats +43 -0
  80. package/test/bats/cartographer-events.bats +267 -0
  81. package/test/bats/cartographer-filter.bats +169 -0
  82. package/test/bats/cartographer-lock.bats +3 -3
  83. package/test/bats/cartographer-omission.bats +202 -0
  84. package/test/bats/cartographer-resolve.bats +287 -0
  85. package/test/bats/cartographer-run-audit.bats +325 -0
  86. package/test/bats/compass-sanitizer.bats +11 -11
  87. package/test/bats/compass-transcript.bats +2 -2
  88. package/test/bats/config.bats +15 -15
  89. package/test/bats/curator-session-start.bats +10 -3
  90. package/test/bats/emit-payload-default.bats +52 -0
  91. package/test/bats/governor-ledger.bats +1 -1
  92. package/test/bats/historian-prompt-submit.bats +1 -1
  93. package/test/bats/inspector-post-write-hook.bats +4 -4
  94. package/test/bats/librarian-author-key.bats +477 -0
  95. package/test/bats/librarian-cli.bats +16 -16
  96. package/test/bats/librarian-lesson-judge.bats +884 -0
  97. package/test/bats/librarian-lesson-promote.bats +552 -0
  98. package/test/bats/librarian-lesson-review.bats +900 -0
  99. package/test/bats/librarian-lesson-transform.bats +17 -3
  100. package/test/bats/librarian-session-end.bats +161 -0
  101. package/test/bats/librarian-session-start.bats +36 -2
  102. package/test/bats/lineage-config.bats +1 -1
  103. package/test/bats/lineage-redact.bats +5 -5
  104. package/test/bats/session-tracker.bats +4 -4
  105. package/test/bats/tribunal-aggregate.bats +354 -1
  106. package/test/bats/tribunal-gate.bats +631 -0
  107. package/test/bats/tribunal-judge-agents.bats +119 -0
  108. package/test/bats/tribunal-jury.bats +1 -1
  109. package/test/bats/turn-tracker.bats +1 -1
  110. package/test/bats/warden-sanitizer.bats +3 -3
  111. package/test/bats/worktree-tracker.bats +2 -2
  112. package/test/node/lesson-validate-agreement.test.mjs +35 -6
@@ -57,6 +57,82 @@ librarian_lesson_valid_range() {
57
57
  [[ "$r" =~ $pattern ]]
58
58
  }
59
59
 
60
+ # Everything both validators check, excluding the scope branch. Kept in one
61
+ # place because it mirrors the vendored sub-schemas line for line; a second
62
+ # copy is how the jq rules and the schema drifted apart once already.
63
+ #
64
+ # Composed into a jq program by each validator, which appends its own scope
65
+ # clause. artifact_ids, session_ids, and observed_at are checked against the
66
+ # same patterns as the vendored lesson-evidence.subschema.json (ULID,
67
+ # non-empty string, RFC3339 date-time) — a provenance-less artifact
68
+ # (session_id/created_at stitched in as "") must fail here, not pass through
69
+ # and get buried permanently once librarian_lesson_seen marks it handled.
70
+ #
71
+ # The `keys - [...] | length == 0` checks mirror `additionalProperties:
72
+ # false` on the vendored `evidence` and `applies_to` sub-schemas, and the
73
+ # `all(type == "string" and length > 0)` checks mirror their array items'
74
+ # `minLength: 1`. Neither is decorative: without them a model that
75
+ # "helpfully" adds an extra field, or emits an empty-string array entry,
76
+ # produces a proposal that passes here but fails ajv against the contract it
77
+ # claims to satisfy — and lessons are meant to be shared with other people.
78
+ # Each `keys` call is guarded by a preceding `type == "object"` check in the
79
+ # same `and` chain: jq's `and` short-circuits left to right, so `keys` on a
80
+ # missing/non-object value is never reached.
81
+ _LIBRARIAN_LESSON_STRUCTURAL='
82
+ (.claim | type) == "string" and (.claim | length) > 0
83
+ and (.rationale | type) == "string" and (.rationale | length) > 0
84
+ and (.evidence | type) == "object"
85
+ and ((.evidence | keys) - ["artifact_ids", "session_ids", "project_key", "observed_at", "resolution"] | length) == 0
86
+ and (.evidence.artifact_ids | type) == "array" and (.evidence.artifact_ids | length) > 0
87
+ and (.evidence.artifact_ids | all(type == "string" and test("^[0-9A-HJKMNP-TV-Z]{26}$")))
88
+ and (.evidence.session_ids | type) == "array" and (.evidence.session_ids | length) > 0
89
+ and (.evidence.session_ids | all(type == "string" and length > 0))
90
+ and (.evidence.project_key | type) == "string"
91
+ and (.evidence.project_key | test("^[0-9a-f]{12}$"))
92
+ and (.evidence.observed_at | type) == "string"
93
+ 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))$"))
94
+ and (.evidence.resolution | type) == "string" and (.evidence.resolution | length) > 0
95
+ and (.applies_to | type) == "object"
96
+ and ((.applies_to | keys) - ["stack", "scope", "file_patterns", "task_kinds"] | length) == 0
97
+ and (.applies_to.stack | type) == "array" and (.applies_to.stack | length) > 0
98
+ and (.applies_to.stack | all(type == "string" and length > 0))
99
+ and (.applies_to.file_patterns | type) == "array"
100
+ and (.applies_to.file_patterns | all(type == "string" and length > 0))
101
+ and (.applies_to.task_kinds | type) == "array"
102
+ and (.applies_to.task_kinds | all(type == "string" and length > 0))
103
+ '
104
+
105
+ # The versioned branch, shared by both validators.
106
+ _LIBRARIAN_LESSON_SCOPE_VERSIONED='
107
+ .applies_to.scope.kind == "versioned"
108
+ and ((.applies_to.scope | keys) - ["kind", "versions"] | length) == 0
109
+ and (.applies_to.scope.versions | type) == "object"
110
+ and (.applies_to.scope.versions | length) > 0
111
+ '
112
+
113
+ # Checks that only apply to a versioned candidate: the cross-field rule JSON
114
+ # Schema cannot express, and the range pattern on each value.
115
+ #
116
+ # NUL-delimited, not newline-delimited: a range value with an embedded newline
117
+ # would otherwise split into two lines that can each pass individually even
118
+ # though the single value they came from is not a valid range. Do not skip
119
+ # empty reads either — jq never emits one for a non-empty object of strings,
120
+ # so an empty read means the range itself is empty, which is invalid.
121
+ _librarian_lesson_check_versions() {
122
+ local candidate="$1"
123
+
124
+ printf '%s' "$candidate" | jq -e '
125
+ (.applies_to.scope.versions | keys) - .applies_to.stack | length == 0
126
+ ' >/dev/null 2>&1 || return 1
127
+
128
+ local range
129
+ while IFS= read -r -d '' range; do
130
+ librarian_lesson_valid_range "$range" || return 1
131
+ done < <(printf '%s' "$candidate" | jq --raw-output0 '.applies_to.scope.versions[]' 2>/dev/null)
132
+
133
+ return 0
134
+ }
135
+
60
136
  # Validate a full candidate. Prints nothing on success; prints a reason slug
61
137
  # to stderr on failure.
62
138
  #
@@ -65,76 +141,69 @@ librarian_lesson_validate_candidate() {
65
141
  local candidate="${1:-}"
66
142
  [[ -z "$candidate" ]] && { printf 'schema_invalid\n' >&2; return 1; }
67
143
 
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
144
+ # versioned ONLY. This is the guarantee that stops the transform minting
145
+ # lessons that never expire: private lessons run no jury, so nothing
146
+ # downstream would catch a bad version_independent claim. A human may
147
+ # assert that branch see librarian_lesson_validate_confirmed — because
148
+ # the constraint in the review path forces it to a judged visibility.
149
+ if ! printf '%s' "$candidate" | jq -e \
150
+ "${_LIBRARIAN_LESSON_STRUCTURAL} and ${_LIBRARIAN_LESSON_SCOPE_VERSIONED}" \
151
+ >/dev/null 2>&1; then
114
152
  printf 'schema_invalid\n' >&2
115
153
  return 1
116
154
  fi
117
155
 
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
156
+ _librarian_lesson_check_versions "$candidate" || {
157
+ printf 'schema_invalid\n' >&2
158
+ return 1
159
+ }
160
+
161
+ return 0
162
+ }
163
+
164
+ # Validate a candidate a human has confirmed.
165
+ #
166
+ # Identical to librarian_lesson_validate_candidate except that it also permits
167
+ # the version_independent branch, which requires a non-empty justification.
168
+ #
169
+ # The two are NOT redundant and the difference is not stylistic. They encode
170
+ # different trust: this one bounds what a human may assert AND a jury will then
171
+ # check, because the review path refuses version_independent at private
172
+ # visibility. The transform's validator bounds what a model may assert
173
+ # unsupervised, where nothing downstream would catch a bad claim. Deleting
174
+ # either collapses that distinction.
175
+ #
176
+ # Usage: librarian_lesson_validate_confirmed <candidate_json>
177
+ librarian_lesson_validate_confirmed() {
178
+ local candidate="${1:-}"
179
+ [[ -z "$candidate" ]] && { printf 'schema_invalid\n' >&2; return 1; }
180
+
181
+ local scope_clause='
182
+ (
183
+ ('"${_LIBRARIAN_LESSON_SCOPE_VERSIONED}"')
184
+ or (
185
+ .applies_to.scope.kind == "version_independent"
186
+ and ((.applies_to.scope | keys) - ["kind", "justification"] | length) == 0
187
+ and (.applies_to.scope.justification | type) == "string"
188
+ and (.applies_to.scope.justification | length) > 0
189
+ )
190
+ )
191
+ '
192
+
193
+ if ! printf '%s' "$candidate" | jq -e \
194
+ "${_LIBRARIAN_LESSON_STRUCTURAL} and ${scope_clause}" \
195
+ >/dev/null 2>&1; then
123
196
  printf 'schema_invalid\n' >&2
124
197
  return 1
125
198
  fi
126
199
 
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)
200
+ # Range and subset rules apply only to the versioned branch.
201
+ if printf '%s' "$candidate" | jq -e '.applies_to.scope.kind == "versioned"' >/dev/null 2>&1; then
202
+ _librarian_lesson_check_versions "$candidate" || {
203
+ printf 'schema_invalid\n' >&2
204
+ return 1
205
+ }
206
+ fi
138
207
 
139
208
  return 0
140
209
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: librarian
3
- description: Review the librarian's pending memory promotion proposals queued from past sessions. Walk pending entries with the user one at a time, surfacing provenance and conflict state, and route each to accept (writes the typed memory file and updates MEMORY.md), reject (writes a body-hash tombstone so the same content won't re-propose), or defer (leave in the queue). Use when the user types `/librarian`, `/librarian review`, `/librarian triage`, `/librarian status`, or `/librarian list`, or asks to review librarian proposals.
3
+ description: Review the librarian's pending memory promotion proposals and lesson candidates queued from past sessions. Walk pending entries with the user one at a time, surfacing provenance and conflict state, and route each to accept (writes the typed memory file and updates MEMORY.md), reject (writes a body-hash tombstone so the same content won't re-propose), or defer (leave in the queue). Lesson candidates route separately to confirm (with a visibility), pass, or defer, with unconfirm to take back a confirmation before the jury sees it. Use when the user types `/librarian`, `/librarian review`, `/librarian triage`, `/librarian status`, `/librarian list`, `/librarian lessons`, `/librarian lessons review`, or `/librarian lessons judge`, or asks to review librarian proposals or lesson candidates.
4
4
  ---
5
5
 
6
6
  # Librarian: Promotion Queue Review
@@ -17,6 +17,9 @@ Read the user's argument after `/librarian`:
17
17
  - `list` → print the pending table and stop
18
18
  - `status` → print one-line counts and stop
19
19
  - a proposal id (starts with a ULID-shaped string) → jump straight to **show** for that id
20
+ - `lessons`, `lessons review` → **walk the lesson queue** (see below)
21
+ - `lessons list` / `lessons status` → print and stop (`lessons list --confirmed` lists confirmed lessons instead of pending ones)
22
+ - `lessons judge` → **run the jury over confirmed candidates** (see below)
20
23
 
21
24
  If the user passes a free-form intent ("clear out the queue", "what's pending?"), map it to `review` or `list` as appropriate.
22
25
 
@@ -26,13 +29,23 @@ Source the plugin helpers and invoke `librarian_cli`. Run this in a single bash
26
29
 
27
30
  ```bash
28
31
  set -uo pipefail
32
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
33
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
29
34
  source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-config.sh"
30
35
  source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-project-key.sh"
31
36
  source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-storage.sh"
32
37
  source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-emit.sh"
38
+ source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-lesson-storage.sh"
39
+ source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-lesson-validate.sh"
40
+ source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-lesson-review.sh"
41
+ source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-lesson-rubric.sh"
42
+ source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-lesson-judge.sh"
43
+ source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-author-key.sh"
44
+ source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-lesson-promote.sh"
33
45
  source "$CLAUDE_PLUGIN_ROOT/scripts/lib/librarian-cli.sh"
34
46
 
35
47
  # action is one of: list | show <id> | accept <id> | reject <id> [reason] | defer <id> | status
48
+ # or: lessons <list [--confirmed]|show <id>|confirm <id> <visibility> [--justification TEXT]|pass <id> [reason]|unconfirm <id>|defer <id>|status>
36
49
  librarian_cli "<action>" "<args...>"
37
50
  ```
38
51
 
@@ -55,9 +68,93 @@ For `review` (the default), loop:
55
68
 
56
69
  For `list` and `status`, just call `librarian_cli <action>` once and render the output.
57
70
 
71
+ ## The lesson walkthrough
72
+
73
+ Lesson candidates are a separate queue from memory proposals — confirming one is a step toward publishing beyond this machine, not writing a local file, so it gets its own walk rather than folding into the loop above.
74
+
75
+ For `lessons` / `lessons review` (default for the `lessons` route), loop:
76
+
77
+ 1. Call `librarian_cli lessons list`. If the output says `No pending lessons.`, tell the user the queue is clear and stop.
78
+ 2. Take the first pending id. Call `librarian_cli lessons show <id>` to render it.
79
+ 3. Present the candidate to the user in plain English, showing: `claim`, `rationale`, `evidence.resolution`, `applies_to.stack`, `scope.versions` (or the scope kind if unversioned), and the source artifact id.
80
+ 4. Ask the user how to route it: **confirm** (with a visibility), **pass** (optionally with a reason), or **defer**.
81
+ 5. Route the answer:
82
+ - **confirm** → `librarian_cli lessons confirm <id> <visibility> [--justification TEXT]`. Confirming **requires** a visibility (`private`, `org`, or `public`) — never call confirm without one. `--justification` rewrites the scope to `version_independent` and requires `org` or `public` visibility: a `private` lesson runs no jury, so its justification would go unchecked. If the user wants version-independence at `private`, tell them why it's refused rather than silently dropping the justification.
83
+ - **pass** → `librarian_cli lessons pass <id> "<reason>"`. The reason is optional.
84
+ - **defer** → `librarian_cli lessons defer <id>`. The candidate stays pending; mention it'll resurface next session.
85
+ 6. After each routed decision, fetch the next pending id and repeat. When `list` returns no rows, finish with `librarian_cli lessons status` so the user sees the final count.
86
+
87
+ A confirm made at the wrong visibility isn't stuck that way — `librarian_cli lessons unconfirm <id>` takes it back, returning the lesson to pending so the user can confirm it again at the right visibility. It only works on a `confirmed` lesson: a `passed` lesson stays passed, because that decision already has its own record and unconfirm won't touch it.
88
+
89
+ Finding the lesson is its own step, because `lessons list` shows only the pending queue and a confirmed lesson is by definition not in it. The user regretting a confirm is usually back in a later session with the claim in mind and no id, so start from `librarian_cli lessons list --confirmed`, which prints the same `<id> <claim>` rows for everything confirmed and not yet judged. Then `librarian_cli lessons show <id>` — its `visibility` line is what tells you which tier the lesson currently sits at, and whether unconfirming is what the user actually wants.
90
+
91
+ For `lessons list`, `lessons list --confirmed`, and `lessons status`, just call `librarian_cli lessons <action>` once and render the output.
92
+
93
+ For `lessons judge`, run the jury over confirmed candidates:
94
+
95
+ 1. Call `librarian_cli lessons list --confirmed --json`. Each row carries `id`,
96
+ `visibility`, and the full `candidate`. If the array is empty, tell the user
97
+ there is nothing awaiting judgment and stop.
98
+ 2. **Report the batch before spending anything.** Say how many candidates are
99
+ confirmed and how many are `public`, and ask whether to proceed. This is the
100
+ most expensive step in the pipeline. If the user declines, stop — nothing is
101
+ written and every candidate stays `confirmed`.
102
+ 3. For each candidate, in order:
103
+ - **If `visibility` is `private`, dispatch no judges at all.** Call
104
+ `librarian_cli lessons judge <id> '[]'` and move on. Private lessons run no
105
+ jury; that is what makes cost scale with intent rather than artifact volume.
106
+ - Otherwise spawn **both** `tribunal-judge-standard` and
107
+ `tribunal-judge-adversarial` with the Task tool. Give each the candidate's
108
+ `claim`, `rationale`, `evidence.resolution`, and `applies_to`, plus the
109
+ rubric criteria for its visibility: for `org`, grounding / scope_accuracy /
110
+ generality; for `public`, those three plus **disclosure** — does the text
111
+ leak a credential, internal hostname, customer name, or proprietary detail?
112
+ - Each judge returns a JSON object with `score`, `passed`, `judge_type`,
113
+ `feedback_summary`, and `criterion_scores` — a map from **each rubric
114
+ criterion name you gave it** to a score in `[0,1]`. **Tell each judge it
115
+ must score every criterion you listed.** Every criterion in both lesson
116
+ rubrics carries a floor, and a floor no judge scored makes the whole panel
117
+ UNJUDGED — the candidate stays `confirmed` and is re-judged, at full cost,
118
+ on the next run. So an omission here does not soften a verdict; it prevents
119
+ one. That differs from tribunal, which degrades to a plain mean instead of
120
+ refusing, and the shared judge agents describe tribunal's behavior.
121
+ A judge that genuinely cannot assess a criterion should say so in
122
+ `feedback_summary` and score its honest worst case rather than omit the
123
+ key. Collect both verdicts into a JSON array **verbatim** — never summarize
124
+ or reconstruct a judge's verdict.
125
+ - Call `librarian_cli lessons judge <id> '<verdicts-json>'`. Record before
126
+ moving to the next candidate, so an interrupted run costs at most one
127
+ re-judgment. Recording a verdict also promotes the lesson in the same
128
+ call — approved candidates land in the pool and rejected ones in the
129
+ declined ledger, with no separate step. If the output instead says the
130
+ lesson was judged but not promoted, the verdict is already recorded;
131
+ retry once with `librarian_cli lessons promote <id>`, not by re-judging —
132
+ re-judging would spend tokens again for a verdict that already exists.
133
+ **If that retry itself fails, do not loop on it.** Add the id to the
134
+ judged-but-not-promoted bucket and move to the next candidate; report it
135
+ at the end alongside the retry command rather than retrying again here.
136
+ 4. **If either judge fails to return parseable JSON, do not invent a verdict and
137
+ do not drop that judge.** The array must still have one entry per empaneled
138
+ judge: put that judge's raw output, as a JSON string, in its slot instead of
139
+ a parsed verdict object. Pass that array to the CLI; it will exit 2, leave
140
+ the candidate `confirmed`, and report that it could not be judged. Collect
141
+ those ids and list them at the end so the user knows to re-run. A broken judge
142
+ must never become a rejection — the artifact's watermark has already moved,
143
+ so a false rejection buries a good lesson permanently.
144
+ 5. Finish by reporting counts: approved, rejected, could-not-judge, and
145
+ **judged but not promoted** (verdict recorded, pool write still pending —
146
+ list these ids with `librarian_cli lessons promote <id>` as the next step).
147
+ Don't fold this bucket into "approved": a lesson here has no pool entry yet.
148
+
149
+ `scope_accuracy` is the criterion that matters most on a `version_independent`
150
+ candidate. The schema guarantees such a lesson **carries** a justification; this
151
+ criterion asks whether it is **true**.
152
+
58
153
  ## Safety rules
59
154
 
60
155
  - **Never accept a proposal on the user's behalf without explicit confirmation.** Accepting writes a file to the user's typed memory store and that memory will be loaded into every future session in this project. Treat each accept like editing a CLAUDE.md.
61
156
  - **Do not edit MEMORY.md directly.** `accept` updates the index for you; hand-editing risks duplicate entries or stale links.
62
157
  - **Do not delete proposal files manually.** Reject (with a tombstone) is the cleanup path. Direct deletion would let the same body re-propose on the next scan.
63
158
  - **Conflict-state proposals deserve a careful read.** When `conflict_state` is `near_duplicate` or `contradicts_existing`, surface the conflict to the user before they decide. Often the right answer is reject (the existing memory is better) or accept-and-then-prune (you can mention that follow-up).
159
+ - **Never confirm a lesson on the user's behalf without an explicit visibility.** Confirming commits a candidate toward leaving this machine — a decision separate from, and heavier than, accepting a memory proposal.
160
+ - **Never dispatch judges without reporting the batch and getting the user's go-ahead first.** Judge dispatch is the most expensive step in the pipeline; report the confirmed and `public` counts and wait before spawning a single judge.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tribunal",
3
- "version": "1.1.2",
3
+ "version": "1.2.7",
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,68 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.7](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.2.6...tribunal-v1.2.7) (2026-08-16)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **tribunal:** let every blocking arm name the floor it tripped over :label: ([#166](https://github.com/onlooker-community/ecosystem/issues/166)) ([1819b5f](https://github.com/onlooker-community/ecosystem/commit/1819b5f7ad0348584bcb0e72e28c9af153ba3a4d))
9
+
10
+ ## [1.2.6](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.2.5...tribunal-v1.2.6) (2026-08-16)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **tribunal:** stop a low criterion from hiding why the panel failed :arrows_counterclockwise: ([#164](https://github.com/onlooker-community/ecosystem/issues/164)) ([a3ec128](https://github.com/onlooker-community/ecosystem/commit/a3ec128f1e2e723e02396580b9ffbcff26f0787f))
16
+
17
+ ## [1.2.5](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.2.4...tribunal-v1.2.5) (2026-08-16)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **tribunal:** a floor you cannot read is a floor you cannot clear :lock: ([#162](https://github.com/onlooker-community/ecosystem/issues/162)) ([4ff2ec0](https://github.com/onlooker-community/ecosystem/commit/4ff2ec0f772af51ad4b5ea631ae2d3c68d229499))
23
+
24
+ ## [1.2.4](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.2.3...tribunal-v1.2.4) (2026-08-16)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **tribunal:** refuse a score that is not on the scale it claims :straight_ruler: ([#160](https://github.com/onlooker-community/ecosystem/issues/160)) ([bab2855](https://github.com/onlooker-community/ecosystem/commit/bab28558a68402c1dd67623f469467fd99302ba6))
30
+
31
+ ## [1.2.3](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.2.2...tribunal-v1.2.3) (2026-08-16)
32
+
33
+
34
+ ### Bug Fixes
35
+
36
+ * **tribunal:** stop a verdict nobody scored from voting to approve :no_entry: ([#158](https://github.com/onlooker-community/ecosystem/issues/158)) ([1d6c2dc](https://github.com/onlooker-community/ecosystem/commit/1d6c2dc9c8f5833891732dc7e6637c496ba81f6f))
37
+
38
+ ## [1.2.2](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.2.1...tribunal-v1.2.2) (2026-08-15)
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * **tribunal:** drop scoreless verdicts instead of averaging them in :bug: ([#154](https://github.com/onlooker-community/ecosystem/issues/154)) ([f546757](https://github.com/onlooker-community/ecosystem/commit/f5467574f08094b0369f0bd699983cb41ded2cc3))
44
+
45
+ ## [1.2.1](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.2.0...tribunal-v1.2.1) (2026-08-15)
46
+
47
+
48
+ ### Bug Fixes
49
+
50
+ * **tribunal:** close two ways a criterion floor could still be escaped :relieved: ([#152](https://github.com/onlooker-community/ecosystem/issues/152)) ([5b2ddee](https://github.com/onlooker-community/ecosystem/commit/5b2ddee8a787ba0f3e8656a6522b12f348f74859))
51
+
52
+ ## [1.2.0](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.1.3...tribunal-v1.2.0) (2026-08-15)
53
+
54
+
55
+ ### Features
56
+
57
+ * **tribunal,librarian:** make rubric weights and min_pass floors real :straight_ruler: ([#150](https://github.com/onlooker-community/ecosystem/issues/150)) ([f8f8e28](https://github.com/onlooker-community/ecosystem/commit/f8f8e28b60f6d13d5a2a54e26ca284137b77f99a))
58
+
59
+ ## [1.1.3](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.1.2...tribunal-v1.1.3) (2026-08-10)
60
+
61
+
62
+ ### Bug Fixes
63
+
64
+ * 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))
65
+
3
66
  ## [1.1.2](https://github.com/onlooker-community/ecosystem/compare/tribunal-v1.1.1...tribunal-v1.1.2) (2026-08-02)
4
67
 
5
68
 
@@ -31,6 +31,32 @@ You are the **Adversarial Judge** in a Tribunal jury. Your job is to try, in goo
31
31
  - A single vague "this might fail" is worth `0.0` — name the input or do not raise it.
32
32
  - If you genuinely cannot falsify, score `0.85+` and say so. Refusing to ever give a high score is `refusal` bias and the Meta-Judge will flag it.
33
33
 
34
+ ## Scoring against the rubric
35
+
36
+ You are given a rubric with named criteria, each carrying a weight and a
37
+ `min_pass` floor. Your falsification work is how you form a judgment; the
38
+ rubric's criteria are how you report it.
39
+
40
+ Report a score in `[0,1]` for every rubric criterion in `criterion_scores`,
41
+ keyed by the rubric's own names. Your `criteria_evaluated` list stays what it
42
+ has always been — the dimensions you probed (edge cases, concurrency,
43
+ idempotency). The two lists are not expected to match.
44
+
45
+ `safety` in particular is a criterion you are well placed to score and no other
46
+ default judge covers. A crash on malformed input, a non-idempotent migration, a
47
+ race that corrupts state — those are safety findings, and this is where they
48
+ belong.
49
+
50
+ **Do not score `0` for a criterion you did not assess.** A `0` says you assessed
51
+ it and it failed, which on a criterion with a floor blocks the task by itself.
52
+
53
+ Omitting the key instead is handled by the caller's policy, and the policies
54
+ differ: tribunal treats it as a coverage gap and falls back to a plain mean,
55
+ while other callers refuse to judge the panel at all when a floored criterion
56
+ went unscored. So an omission is not the safe middle option — prefer scoring
57
+ your honest worst case and saying what you could not assess in
58
+ `feedback_summary`.
59
+
34
60
  ## Output format
35
61
 
36
62
  Final message is a single JSON object — no prose, no fence:
@@ -41,6 +67,12 @@ Final message is a single JSON object — no prose, no fence:
41
67
  "passed": false,
42
68
  "judge_type": "adversarial",
43
69
  "criteria_evaluated": ["edge-cases", "concurrency", "idempotency"],
70
+ "criterion_scores": {
71
+ "correctness": 0.5,
72
+ "completeness": 0.6,
73
+ "safety": 0.55,
74
+ "clarity": 0.8
75
+ },
44
76
  "strengths_count": 1,
45
77
  "weaknesses_count": 2,
46
78
  "confidence": 0.8,
@@ -48,4 +80,6 @@ Final message is a single JSON object — no prose, no fence:
48
80
  }
49
81
  ```
50
82
 
83
+ The `criterion_scores` keys above are the **default rubric's** criteria, shown as an example. **The rubric you are given governs** — score its criterion names, whatever they are. Other callers ship different rubrics; librarian's lesson rubrics, for instance, use `grounding`, `scope_accuracy`, `generality`, and `disclosure`.
84
+
51
85
  `feedback_summary` should describe each falsification with enough specificity that the Actor can reproduce it on retry.
@@ -27,6 +27,31 @@ You are the **Security Judge** in a Tribunal jury. Score the Actor's output excl
27
27
  - Read the changed files. Do not score from the summary.
28
28
  - Do not flag style or hypothetical "could be exploited if…" without a concrete attack chain. The Meta-Judge will mark you as `biased` if you over-report.
29
29
 
30
+ ## Scoring against the rubric
31
+
32
+ You are given a rubric with named criteria, each carrying a weight and a
33
+ `min_pass` floor. Your findings are how you form a judgment; the rubric's
34
+ criteria are how you report it.
35
+
36
+ Report a score in `[0,1]` for every rubric criterion in `criterion_scores`,
37
+ keyed by the rubric's own names. Your `criteria_evaluated` list stays what it
38
+ has always been — the dimensions you swept (injection, secrets, path
39
+ traversal). The two lists are not expected to match.
40
+
41
+ `safety` is where your findings land. It carries the highest floor in the
42
+ default rubric, and a single unresolved injection or leaked credential should
43
+ put your `safety` score below it.
44
+
45
+ **Do not score `0` for a criterion you did not assess.** A `0` says you assessed
46
+ it and it failed, which on a criterion with a floor blocks the task by itself.
47
+
48
+ Omitting the key instead is handled by the caller's policy, and the policies
49
+ differ: tribunal treats it as a coverage gap and falls back to a plain mean,
50
+ while other callers refuse to judge the panel at all when a floored criterion
51
+ went unscored. So an omission is not the safe middle option — prefer scoring
52
+ your honest worst case and saying what you could not assess in
53
+ `feedback_summary`.
54
+
30
55
  ## Output format
31
56
 
32
57
  Final message is a single JSON object — no prose, no fence:
@@ -37,6 +62,12 @@ Final message is a single JSON object — no prose, no fence:
37
62
  "passed": false,
38
63
  "judge_type": "security",
39
64
  "criteria_evaluated": ["injection", "secrets", "path-traversal"],
65
+ "criterion_scores": {
66
+ "correctness": 0.7,
67
+ "completeness": 0.6,
68
+ "safety": 0.2,
69
+ "clarity": 0.75
70
+ },
40
71
  "strengths_count": 1,
41
72
  "weaknesses_count": 2,
42
73
  "confidence": 0.9,
@@ -44,4 +75,6 @@ Final message is a single JSON object — no prose, no fence:
44
75
  }
45
76
  ```
46
77
 
78
+ The `criterion_scores` keys above are the **default rubric's** criteria, shown as an example. **The rubric you are given governs** — score its criterion names, whatever they are. Other callers ship different rubrics; librarian's lesson rubrics, for instance, use `grounding`, `scope_accuracy`, `generality`, and `disclosure`.
79
+
47
80
  When `passed: false`, every finding in `feedback_summary` must point at a file and (when possible) a line. Vague security objections waste the Actor's retry budget.
@@ -33,6 +33,12 @@ Your **final message** must be a single JSON object matching `TribunalVerdictPay
33
33
  "passed": true,
34
34
  "judge_type": "standard",
35
35
  "criteria_evaluated": ["correctness", "completeness", "clarity"],
36
+ "criterion_scores": {
37
+ "correctness": 0.9,
38
+ "completeness": 0.75,
39
+ "safety": 0.85,
40
+ "clarity": 0.8
41
+ },
36
42
  "strengths_count": 3,
37
43
  "weaknesses_count": 1,
38
44
  "confidence": 0.85,
@@ -40,8 +46,14 @@ Your **final message** must be a single JSON object matching `TribunalVerdictPay
40
46
  }
41
47
  ```
42
48
 
49
+ The `criterion_scores` keys above are the **default rubric's** criteria, shown as an example. **The rubric you are given governs** — score its criterion names, whatever they are. Other callers ship different rubrics; librarian's lesson rubrics, for instance, use `grounding`, `scope_accuracy`, `generality`, and `disclosure`.
50
+
43
51
  Required fields: `score`, `passed`, `judge_type`. `passed` reflects your own judgment based on the rubric thresholds — the orchestrator may still aggregate and override per gate policy.
44
52
 
53
+ `criterion_scores` maps **each criterion name from the rubric you were given** to your score for it in `[0,1]`. This is separate from `criteria_evaluated`, which lists the dimensions *you* chose to investigate — the rubric's criteria are what the orchestrator weights and floors.
54
+
55
+ Score every rubric criterion you can judge. **Omit any criterion you genuinely cannot assess — do not send `0` for it.** A `0` means "I assessed this and it failed"; an omission means "I did not assess this." The orchestrator treats them very differently: a `0` on a criterion with a floor blocks the task outright, while an omission is handled by the caller's policy. Tribunal reports it as a coverage gap and falls back to a plain mean; other callers refuse to judge a panel at all when a floored criterion went unscored. Do not treat an omission as the safe choice — say what you could not assess in `feedback_summary`.
56
+
45
57
  `feedback_summary` should be 1–3 sentences. Name specific files and lines when you can. This is what the Actor sees on retry.
46
58
 
47
59
  The orchestrator will inject `judge_id` and `iteration_id` when persisting your verdict.
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "rubric": {
30
30
  "default_id": "default",
31
+ "min_criterion_coverage": 0.6,
31
32
  "builtins": [
32
33
  {
33
34
  "id": "default",