@onlooker-community/ecosystem 0.34.1 → 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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.github/workflows/release.yml +14 -14
  3. package/.release-please-manifest.json +4 -4
  4. package/AGENTS.md +5 -0
  5. package/CHANGELOG.md +168 -0
  6. package/CLAUDE.md +5 -0
  7. package/docs/lesson-promotion-pipeline.md +6 -6
  8. package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
  9. package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
  10. package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
  11. package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
  12. package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
  13. package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
  14. package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
  15. package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
  16. package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
  17. package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
  18. package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
  19. package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
  20. package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
  21. package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
  22. package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
  23. package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
  24. package/package.json +2 -2
  25. package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
  26. package/plugins/cartographer/CHANGELOG.md +54 -0
  27. package/plugins/cartographer/README.md +56 -1
  28. package/plugins/cartographer/config.json +7 -1
  29. package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
  30. package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
  31. package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
  32. package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
  33. package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
  34. package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
  35. package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
  36. package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
  37. package/plugins/cartographer/scripts/run-audit.sh +136 -53
  38. package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
  39. package/plugins/librarian/.claude-plugin/plugin.json +1 -1
  40. package/plugins/librarian/CHANGELOG.md +64 -0
  41. package/plugins/librarian/config.json +30 -1
  42. package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
  43. package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
  44. package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
  45. package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
  46. package/plugins/librarian/scripts/lib/librarian-cli.sh +344 -1
  47. package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
  48. package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
  49. package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
  50. package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
  51. package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
  52. package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
  53. package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
  54. package/plugins/librarian/skills/librarian/SKILL.md +98 -1
  55. package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
  56. package/plugins/tribunal/CHANGELOG.md +56 -0
  57. package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
  58. package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
  59. package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
  60. package/plugins/tribunal/config.json +1 -0
  61. package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -11
  62. package/plugins/tribunal/scripts/lib/tribunal-gate.sh +193 -15
  63. package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
  64. package/test/bats/cartographer-config.bats +43 -0
  65. package/test/bats/cartographer-events.bats +267 -0
  66. package/test/bats/cartographer-filter.bats +169 -0
  67. package/test/bats/cartographer-omission.bats +202 -0
  68. package/test/bats/cartographer-resolve.bats +287 -0
  69. package/test/bats/cartographer-run-audit.bats +325 -0
  70. package/test/bats/librarian-author-key.bats +477 -0
  71. package/test/bats/librarian-lesson-judge.bats +884 -0
  72. package/test/bats/librarian-lesson-promote.bats +552 -0
  73. package/test/bats/librarian-lesson-review.bats +900 -0
  74. package/test/bats/librarian-lesson-transform.bats +17 -3
  75. package/test/bats/librarian-session-end.bats +161 -0
  76. package/test/bats/librarian-session-start.bats +34 -0
  77. package/test/bats/tribunal-aggregate.bats +354 -1
  78. package/test/bats/tribunal-gate.bats +631 -0
  79. package/test/bats/tribunal-judge-agents.bats +119 -0
  80. package/test/node/lesson-validate-agreement.test.mjs +35 -6
@@ -8,7 +8,7 @@
8
8
  # 1. discover — collect all auditable files
9
9
  # 2. extract — per-file content hash (incremental cache key)
10
10
  # 3. relate — contradiction + dead_rule analysis (LLM)
11
- # 4. synthesize — stale_ref + scope_collision + finding hash computation
11
+ # 4. synthesize — stale_ref + scope_collision + undocumented_entity + hash
12
12
  # 5. emit — persist findings atomically, emit events for new findings
13
13
  #
14
14
  # Environment:
@@ -28,13 +28,18 @@ source "$PLUGIN_ROOT/scripts/lib/cartographer-config.sh"
28
28
  source "$PLUGIN_ROOT/scripts/lib/cartographer-ulid.sh"
29
29
  source "$PLUGIN_ROOT/scripts/lib/cartographer-project-key.sh"
30
30
  source "$PLUGIN_ROOT/scripts/lib/cartographer-events.sh"
31
+ source "$PLUGIN_ROOT/scripts/lib/cartographer-resolve.sh"
32
+ source "$PLUGIN_ROOT/scripts/lib/cartographer-filter.sh"
31
33
  source "$PLUGIN_ROOT/scripts/lib/cartographer-collect.sh"
32
34
  source "$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh"
35
+ source "$PLUGIN_ROOT/scripts/lib/cartographer-omission.sh"
33
36
 
34
37
  CARTOGRAPHER_DIR="${CARTOGRAPHER_DIR:?CARTOGRAPHER_DIR must be set}"
35
38
  TRIGGER="${CARTOGRAPHER_TRIGGER:-manual}"
36
39
  TARGET_FILE="${CARTOGRAPHER_TARGET_FILE:-}"
37
40
  REPO_ROOT="${CARTOGRAPHER_REPO_ROOT:-$(pwd)}"
41
+ TYPE_FILTER="${CARTOGRAPHER_TYPE_FILTER:-}"
42
+ SCOPE_PATH="${CARTOGRAPHER_SCOPE_PATH:-}"
38
43
  AUDIT_ID=$(cartographer_ulid)
39
44
  START_TS=$(date +%s)
40
45
 
@@ -49,6 +54,16 @@ PHASES_COMPLETED=()
49
54
  PHASES_FAILED=()
50
55
  ALL_FINDINGS="[]"
51
56
 
57
+ # Load config before any accessor runs. Without this every accessor below falls
58
+ # through to its hardcoded default, so exclude_paths, the timeouts, the models,
59
+ # and the token caps were all silently unconfigurable — the orchestrator was the
60
+ # one place that never loaded (ecosystem-88v).
61
+ #
62
+ # This is the single load for the whole audit. The analysis sub-shells take
63
+ # their settings as positional parameters and read no config of their own, so
64
+ # the values resolved here are the ones that take effect everywhere.
65
+ cartographer_config_load "$REPO_ROOT"
66
+
52
67
  _phase_timeout=$(cartographer_config_phase_timeout)
53
68
  _total_timeout=$(cartographer_config_total_timeout)
54
69
  log() { printf '[cartographer] %s\n' "$*" >>"$CARTOGRAPHER_DIR/audit.log" 2>&1; }
@@ -59,11 +74,36 @@ _model_synthesis=$(cartographer_config_model_synthesis)
59
74
  _max_tokens_extraction=$(cartographer_config_max_output_tokens_extraction)
60
75
  _max_tokens_synthesis=$(cartographer_config_max_output_tokens_synthesis)
61
76
  _exclude_json=$(cartographer_config_exclude_paths)
77
+ _undocumented_enabled=$(cartographer_config_undocumented_enabled)
78
+ _undocumented_globs=$(cartographer_config_undocumented_globs)
79
+ _undocumented_exclude=$(cartographer_config_undocumented_exclude)
80
+ _undocumented_max=$(cartographer_config_undocumented_max_findings)
62
81
 
63
82
  emit_safe() {
64
83
  cartographer_emit_event "$1" "$2" 2>>"$CARTOGRAPHER_DIR/audit.log" || true
65
84
  }
66
85
 
86
+ # Announce one finding the resolution sweep retired.
87
+ #
88
+ # Passed to cartographer_resolve_absent_findings by name rather than called
89
+ # after it, so the announcement happens inside the sweep's own guards — a run
90
+ # that must not resolve cannot announce either. Handing over a function name
91
+ # also keeps cartographer-resolve.sh free of any dependency on the event bus.
92
+ _emit_resolved() {
93
+ emit_safe "cartographer.issue.resolved" \
94
+ "$(cartographer_issue_resolved_payload "$AUDIT_ID" "$1")"
95
+ }
96
+
97
+ # Reject an unrecognized type rather than filtering everything away. An empty
98
+ # audit is indistinguishable from a clean repo, so a typo would read as good
99
+ # news — the same class of silent-nothing this flag was filed for.
100
+ if [[ -n "$TYPE_FILTER" ]] && ! cartographer_filter_valid_type "$TYPE_FILTER"; then
101
+ log "error: unknown type filter '${TYPE_FILTER}' (expected one of: ${CARTOGRAPHER_FINDING_TYPES})"
102
+ exit 1
103
+ fi
104
+ [[ -n "$TYPE_FILTER" ]] && log "filter type=${TYPE_FILTER}"
105
+ [[ -n "$SCOPE_PATH" ]] && log "filter scope=${SCOPE_PATH}"
106
+
67
107
  # ── Phase 1: Discover ──────────────────────────────────────────────────────────
68
108
  run_discover() {
69
109
  log "phase=discover starting"
@@ -82,6 +122,14 @@ run_discover() {
82
122
  GLOBAL_FILES=$(printf '%s\n' "$raw_global" | grep -v '^$' | jq -R . | jq -s .)
83
123
  fi
84
124
 
125
+ # Scope narrows what gets analyzed, not what the repo root is: stale_ref
126
+ # resolves path tokens against the real root, and re-rooting would make
127
+ # every reference outside the scope look broken.
128
+ if [[ -n "$SCOPE_PATH" ]]; then
129
+ DISCOVERED_FILES=$(cartographer_filter_scope \
130
+ "$DISCOVERED_FILES" "$REPO_ROOT" "$SCOPE_PATH")
131
+ fi
132
+
85
133
  local file_count
86
134
  file_count=$(printf '%s' "$DISCOVERED_FILES" | jq 'length')
87
135
  log "phase=discover files=${file_count}"
@@ -119,11 +167,19 @@ run_extract() {
119
167
  # ── Phase 3: Relate (contradiction + dead_rule) ────────────────────────────────
120
168
  run_relate() {
121
169
  log "phase=relate starting"
170
+
171
+ # One LLM pass emits both contradiction and dead_rule, so asking for either
172
+ # runs it and the unwanted type is filtered out of the result below.
173
+ if ! cartographer_filter_wants "contradiction" "$TYPE_FILTER"; then
174
+ log "phase=relate skipped by type filter"
175
+ RELATE_FINDINGS="[]"
176
+ PHASES_COMPLETED+=("relate")
177
+ return 0
178
+ fi
179
+
122
180
  local findings
123
181
  findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
124
- "source '$PLUGIN_ROOT/scripts/lib/cartographer-config.sh'
125
- source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
126
- cartographer_config_load '$REPO_ROOT'
182
+ "source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
127
183
  cartographer_analyze_contradiction '$DISCOVERED_FILES' \
128
184
  '$_model_extraction' '$_max_tokens_extraction' '$_phase_timeout'" \
129
185
  2>>"$CARTOGRAPHER_DIR/audit.log") || {
@@ -131,7 +187,7 @@ run_relate() {
131
187
  PHASES_FAILED+=("relate")
132
188
  return 1
133
189
  }
134
- RELATE_FINDINGS="${findings:-[]}"
190
+ RELATE_FINDINGS=$(cartographer_filter_findings "${findings:-[]}" "$TYPE_FILTER")
135
191
  local count
136
192
  count=$(printf '%s' "$RELATE_FINDINGS" | jq 'length' 2>/dev/null || printf '0')
137
193
  log "phase=relate findings=${count}"
@@ -142,22 +198,39 @@ run_relate() {
142
198
  run_synthesize() {
143
199
  log "phase=synthesize starting"
144
200
 
145
- local stale_findings scope_findings
146
- stale_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
147
- "source '$PLUGIN_ROOT/scripts/lib/cartographer-config.sh'
148
- source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
149
- cartographer_config_load '$REPO_ROOT'
150
- cartographer_analyze_stale_ref '$DISCOVERED_FILES' '$REPO_ROOT' \
151
- '$_model_synthesis' '$_max_tokens_synthesis' '$_phase_timeout'" \
152
- 2>>"$CARTOGRAPHER_DIR/audit.log") || stale_findings="[]"
153
-
154
- scope_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
155
- "source '$PLUGIN_ROOT/scripts/lib/cartographer-config.sh'
156
- source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
157
- cartographer_config_load '$REPO_ROOT'
158
- cartographer_analyze_scope_collision '$GLOBAL_FILES' '$DISCOVERED_FILES' \
159
- '$_model_synthesis' '$_max_tokens_synthesis' '$_phase_timeout'" \
160
- 2>>"$CARTOGRAPHER_DIR/audit.log") || scope_findings="[]"
201
+ # Each analyzer skipped under a type filter is an LLM call not made, which
202
+ # is where the flag earns its keep.
203
+ local stale_findings="[]" scope_findings="[]"
204
+ if cartographer_filter_wants "stale_ref" "$TYPE_FILTER"; then
205
+ stale_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
206
+ "source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
207
+ cartographer_analyze_stale_ref '$DISCOVERED_FILES' '$REPO_ROOT' \
208
+ '$_model_synthesis' '$_max_tokens_synthesis' '$_phase_timeout'" \
209
+ 2>>"$CARTOGRAPHER_DIR/audit.log") || stale_findings="[]"
210
+ fi
211
+
212
+ if cartographer_filter_wants "scope_collision" "$TYPE_FILTER"; then
213
+ scope_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
214
+ "source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
215
+ cartographer_analyze_scope_collision '$GLOBAL_FILES' '$DISCOVERED_FILES' \
216
+ '$_model_synthesis' '$_max_tokens_synthesis' '$_phase_timeout'" \
217
+ 2>>"$CARTOGRAPHER_DIR/audit.log") || scope_findings="[]"
218
+ fi
219
+
220
+ # Disk → doc. Skipped on targeted post-write audits: DISCOVERED_FILES is a
221
+ # single file there, so grepping it for every entity name would report
222
+ # nearly the whole enumeration as undocumented, and the emit phase would
223
+ # dedup-sentinel those false findings permanently. scope_collision already
224
+ # no-ops on targeted runs for the same reason.
225
+ local omission_findings="[]"
226
+ if [[ -z "$TARGET_FILE" && "$_undocumented_enabled" == "true" ]] \
227
+ && cartographer_filter_wants "undocumented_entity" "$TYPE_FILTER"; then
228
+ omission_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
229
+ "source '$PLUGIN_ROOT/scripts/lib/cartographer-omission.sh'
230
+ cartographer_analyze_undocumented_entity '$DISCOVERED_FILES' '$REPO_ROOT' \
231
+ '$_undocumented_globs' '$_undocumented_exclude' '$_undocumented_max'" \
232
+ 2>>"$CARTOGRAPHER_DIR/audit.log") || omission_findings="[]"
233
+ fi
161
234
 
162
235
  # Merge all raw findings
163
236
  local raw_all
@@ -165,7 +238,8 @@ run_synthesize() {
165
238
  --argjson relate "${RELATE_FINDINGS:-[]}" \
166
239
  --argjson stale "${stale_findings:-[]}" \
167
240
  --argjson scope "${scope_findings:-[]}" \
168
- '$relate + $stale + $scope')
241
+ --argjson omission "${omission_findings:-[]}" \
242
+ '$relate + $stale + $scope + $omission')
169
243
 
170
244
  # Add finding_hash to each finding
171
245
  ALL_FINDINGS="[]"
@@ -213,14 +287,11 @@ run_emit() {
213
287
  now=$(date +%s)
214
288
 
215
289
  if [[ -f "$dedup_sentinel" ]]; then
216
- # Known finding — update last_seen_at atomically, no bus event
290
+ # Known finding — refresh it atomically, no bus event. Refreshing
291
+ # also reopens it; see cartographer_refresh_finding for why that is
292
+ # load-bearing rather than tidiness.
217
293
  (( known_count++ )) || true
218
- if [[ -f "$finding_file" ]]; then
219
- local updated
220
- updated=$(jq --argjson ts "$now" '.last_seen_at=$ts' "$finding_file" 2>/dev/null) || true
221
- [[ -n "$updated" ]] && printf '%s\n' "$updated" >"${finding_file}.tmp" \
222
- && mv -f "${finding_file}.tmp" "$finding_file"
223
- fi
294
+ cartographer_refresh_finding "$finding_file" "$now" || true
224
295
  else
225
296
  # New finding — write file atomically, emit bus event, then mark dedup
226
297
  local with_ts
@@ -229,22 +300,20 @@ run_emit() {
229
300
  printf '%s\n' "$with_ts" >"${finding_file}.tmp"
230
301
  mv -f "${finding_file}.tmp" "$finding_file"
231
302
 
232
- local ftype fseverity ffile_a ffile_b fdesc
233
- ftype=$(printf '%s' "$finding" | jq -r '.type // "unknown"')
234
- fseverity=$(printf '%s' "$finding" | jq -r '.severity // "warning"')
235
- ffile_a=$(printf '%s' "$finding" | jq -r '.file_a // ""')
236
- ffile_b=$(printf '%s' "$finding" | jq -r '.file_b // null')
237
- fdesc=$(printf '%s' "$finding" | jq -r '.description // ""')
238
-
239
- emit_safe "cartographer.issue.found" "$(jq -n \
240
- --arg audit_id "$AUDIT_ID" \
241
- --arg finding_hash "$fhash" \
242
- --arg finding_type "$ftype" \
243
- --arg severity "$fseverity" \
244
- --argjson affected_files "$(jq -n --arg a "$ffile_a" --arg b "$ffile_b" \
245
- 'if $b == "null" or $b == "" then [$a] else [$a,$b] end')" \
246
- --arg summary "$fdesc" \
247
- '{"audit_id":$audit_id,"finding_hash":$finding_hash,"finding_type":$finding_type,"severity":$severity,"affected_files":$affected_files,"summary":$summary}')"
303
+ # Branch on the builder rather than passing its output straight
304
+ # to emit_safe. A typeless finding makes it return non-zero, and
305
+ # an unchecked substitution would hand emit_safe an empty payload
306
+ # that cartographer_emit_event rejects silently relocating the
307
+ # silence this fix exists to end (ecosystem-ci0). Its stderr is
308
+ # appended to the same audit.log emit_safe writes to, because the
309
+ # substitution runs before emit_safe and escapes that redirect.
310
+ local found_payload
311
+ if found_payload=$(cartographer_issue_found_payload \
312
+ "$AUDIT_ID" "$fhash" "$finding" 2>>"$CARTOGRAPHER_DIR/audit.log"); then
313
+ emit_safe "cartographer.issue.found" "$found_payload"
314
+ else
315
+ log "emit: no issue.found for ${fhash} finding carries no type"
316
+ fi
248
317
 
249
318
  touch "$dedup_sentinel"
250
319
  (( new_count++ )) || true
@@ -253,6 +322,23 @@ run_emit() {
253
322
 
254
323
  log "phase=emit new=${new_count} known=${known_count}"
255
324
 
325
+ # Retire findings this run did not observe. Guarded inside against targeted
326
+ # audits and partial runs, both of which see too little to treat absence as
327
+ # evidence the drift is gone.
328
+ local resolved_count
329
+ resolved_count=$(cartographer_resolve_absent_findings \
330
+ "$FINDINGS_DIR" "$START_TS" "$TARGET_FILE" "${#PHASES_FAILED[@]}" "" _emit_resolved)
331
+ log "phase=emit resolved=${resolved_count}"
332
+
333
+ # Only a run that actually swept may report a count. A targeted or partial
334
+ # run leaves the field off entirely rather than reporting 0, which would
335
+ # read as "swept, found nothing to retire". This asks the same predicate the
336
+ # sweep guards on, so the two cannot drift apart on what the run proved.
337
+ local resolved_arg=""
338
+ if cartographer_resolution_is_sound "$TARGET_FILE" "${#PHASES_FAILED[@]}"; then
339
+ resolved_arg="$resolved_count"
340
+ fi
341
+
256
342
  local end_ts duration_ms total_count
257
343
  end_ts=$(date +%s)
258
344
  duration_ms=$(( (end_ts - START_TS) * 1000 ))
@@ -265,20 +351,17 @@ run_emit() {
265
351
  --arg trigger "$TRIGGER" \
266
352
  --argjson new_finding_count "$new_count" \
267
353
  --argjson known_finding_count "$known_count" \
354
+ --argjson resolved_finding_count "$resolved_count" \
268
355
  --argjson total_finding_count "$total_count" \
269
356
  --argjson duration_ms "$duration_ms" \
270
357
  --argjson phases_completed "$(printf '%s\n' "${PHASES_COMPLETED[@]}" | jq -R . | jq -s .)" \
271
358
  --argjson phases_failed "$(printf '%s\n' "${PHASES_FAILED[@]:-}" | jq -R . | jq -s .)" \
272
- '{"audit_id":$audit_id,"trigger":$trigger,"new_finding_count":$new_finding_count,"known_finding_count":$known_finding_count,"total_finding_count":$total_finding_count,"duration_ms":$duration_ms,"phases_completed":$phases_completed,"phases_failed":$phases_failed}' \
359
+ '{"audit_id":$audit_id,"trigger":$trigger,"new_finding_count":$new_finding_count,"known_finding_count":$known_finding_count,"resolved_finding_count":$resolved_finding_count,"total_finding_count":$total_finding_count,"duration_ms":$duration_ms,"phases_completed":$phases_completed,"phases_failed":$phases_failed}' \
273
360
  >"${run_file}.tmp" && mv -f "${run_file}.tmp" "$run_file"
274
361
 
275
- emit_safe "cartographer.audit.complete" "$(jq -n \
276
- --arg audit_id "$AUDIT_ID" \
277
- --arg trigger "$TRIGGER" \
278
- --argjson new_finding_count "$new_count" \
279
- --argjson total_finding_count "$total_count" \
280
- --argjson duration_ms "$duration_ms" \
281
- '{"audit_id":$audit_id,"trigger":$trigger,"new_finding_count":$new_finding_count,"total_finding_count":$total_finding_count,"duration_ms":$duration_ms}')"
362
+ emit_safe "cartographer.audit.complete" \
363
+ "$(cartographer_audit_complete_payload \
364
+ "$AUDIT_ID" "$TRIGGER" "$new_count" "$total_count" "$duration_ms" "$resolved_arg")"
282
365
 
283
366
  PHASES_COMPLETED+=("emit")
284
367
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cartographer
3
- description: Audit CLAUDE.md, AGENTS.md, and .claude/rules/ instruction files for contradictions, stale references, dead rules, and scope collisions. Runs a full audit in the foreground (with lock). Use when the user explicitly invokes /cartographer, or when they want immediate feedback after editing an instruction file. Supports --scope, --phase, --verbose, --status, and --force flags.
3
+ description: Audit CLAUDE.md, AGENTS.md, and .claude/rules/ instruction files for contradictions, stale references, dead rules, scope collisions, and entities no instruction file mentions. Runs a full audit in the foreground (with lock). Use when the user explicitly invokes /cartographer, or when they want immediate feedback after editing an instruction file. Supports --scope, --type, --verbose, --status, and --force flags.
4
4
  ---
5
5
 
6
6
  # Cartographer Skill
@@ -55,14 +55,15 @@ After the audit completes, read findings and render them to the conversation gro
55
55
 
56
56
  ### `/cartographer --verbose` — all findings
57
57
 
58
- Shows ALL known findings (new + previously seen) from `$CARTOGRAPHER_DIR/findings/`. Does NOT re-emit bus events — renders to in-conversation output only.
58
+ Shows ALL known findings (new + previously seen) from `$CARTOGRAPHER_DIR/findings/`, including ones already resolved — that is what makes this the verbose view. Resolved findings are tagged so "did my fix take?" is answerable here. Does NOT re-emit bus events — renders to in-conversation output only.
59
59
 
60
60
  ```bash
61
61
  echo "## Known Cartographer Findings"
62
62
  echo ""
63
63
  for f in "$CARTOGRAPHER_DIR/findings/"*.json; do
64
64
  [[ -f "$f" ]] || continue
65
- jq -r '"**[\(.severity | ascii_upcase)]** \(.type) \(.description)\n Files: \(.file_a // "n/a") / \(.file_b // "n/a")\n Fix: \(.suggested_fix // "n/a")\n"' "$f" 2>/dev/null
65
+ jq -r '(if .resolved == true then "[RESOLVED] " else "" end) as $r |
66
+ "\($r)**[\(.severity | ascii_upcase)]** \(.type) — \(.description)\n Files: \(.file_a // "n/a") / \(.file_b // "n/a")\n Fix: \(.suggested_fix // "n/a")\n"' "$f" 2>/dev/null
66
67
  done
67
68
  ```
68
69
 
@@ -86,7 +87,10 @@ fi
86
87
 
87
88
  runs=$(ls "$CARTOGRAPHER_DIR/runs/" 2>/dev/null | wc -l | tr -d ' ')
88
89
  total=$(ls "$CARTOGRAPHER_DIR/findings/" 2>/dev/null | wc -l | tr -d ' ')
89
- echo "Total findings on disk: $total"
90
+ open=$(cat "$CARTOGRAPHER_DIR/findings/"*.json 2>/dev/null \
91
+ | jq -s '[.[] | select(.resolved != true)] | length' 2>/dev/null || echo 0)
92
+ echo "Open findings: $open"
93
+ echo "Total findings on disk: $total (includes resolved)"
90
94
  echo "Audit runs recorded: $runs"
91
95
  ```
92
96
 
@@ -107,18 +111,37 @@ fi
107
111
  # Then proceed as a normal full audit
108
112
  ```
109
113
 
110
- ### `/cartographer --phase=<phase>` — single phase
114
+ ### `/cartographer --type=<type>` — single finding type
111
115
 
112
- Runs only one analysis phase: `contradiction`, `stale_ref`, `dead_rule`, or `scope_collision`.
113
- Pass `CARTOGRAPHER_PHASE_FILTER` to run-audit.sh (the script checks this env var and skips other phases).
116
+ Narrows the audit to one finding type: `contradiction`, `dead_rule`, `stale_ref`, `scope_collision`, or `undocumented_entity`. Export `CARTOGRAPHER_TYPE_FILTER` before running.
117
+
118
+ ```bash
119
+ export CARTOGRAPHER_TYPE_FILTER="stale_ref"
120
+ ```
121
+
122
+ Analyzers that cannot produce the requested type are skipped entirely, so this is cheaper than a full audit — each skipped analyzer is an LLM call not made. Useful for re-checking one class of drift after fixing it without paying for the rest.
123
+
124
+ One caveat, because the saving is not uniform: `contradiction` and `dead_rule` come out of a single LLM pass, so asking for either runs that pass and discards the other type from the results. Requesting one of those two costs the same as running both; the other three types are genuinely skippable.
125
+
126
+ An unrecognized type is an error rather than a filter that matches nothing — an empty audit looks exactly like a clean repo, so a typo would read as good news.
114
127
 
115
128
  ### `/cartographer --scope=<path>` — scoped audit
116
129
 
117
- Limits discovery to files under `<path>`. Set `CARTOGRAPHER_SCOPE_PATH` before running.
130
+ Limits analysis to instruction files under `<path>`, relative to the repo root or absolute. Export `CARTOGRAPHER_SCOPE_PATH` before running.
131
+
132
+ ```bash
133
+ export CARTOGRAPHER_SCOPE_PATH="plugins/tribunal"
134
+ ```
135
+
136
+ The scope narrows which files are analyzed; it does not become the repo root. `stale_ref` resolves path-like tokens against the real root, and re-rooting would report every reference outside the scope as broken.
137
+
138
+ Both flags compose, and both apply to manual runs only — the SessionStart and PostToolUse hooks always run unfiltered.
118
139
 
119
140
  ## Rendering Findings
120
141
 
121
- After a manual audit completes, render findings grouped by severity:
142
+ After a manual audit completes, render findings grouped by severity. Resolved
143
+ findings are skipped — a finding whose drift the user already fixed must stop
144
+ appearing here, or the advice to fix it never stops being given:
122
145
 
123
146
  ```bash
124
147
  echo "## Cartographer Findings"
@@ -129,6 +152,7 @@ for severity in error warning informational; do
129
152
  output=""
130
153
  for f in "$CARTOGRAPHER_DIR/findings/"*.json; do
131
154
  [[ -f "$f" ]] || continue
155
+ jq -e '.resolved != true' "$f" >/dev/null 2>&1 || continue
132
156
  fsev=$(jq -r '.severity // "warning"' "$f" 2>/dev/null)
133
157
  [[ "$fsev" != "$severity" ]] && continue
134
158
  (( count++ )) || true
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librarian",
3
- "version": "0.7.1",
3
+ "version": "0.13.2",
4
4
  "description": "Consolidation layer between archivist's per-session artifacts and the user's durable typed memory store. Detects which session decisions, dead-ends, and open questions deserve to live across sessions, classifies them into the user/feedback/project/reference types, and queues them as proposals for explicit confirmation. Auto-promotion is opt-in. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,69 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.2](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.13.1...librarian-v0.13.2) (2026-08-18)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **librarian:** put budget skips on the bus at last :satellite: ([#180](https://github.com/onlooker-community/ecosystem/issues/180)) ([bc91749](https://github.com/onlooker-community/ecosystem/commit/bc917495dab053cba825ea28ccdd1993fdacb3e9))
9
+ * **librarian:** stop stage 5 from holding a session open :hourglass: ([#178](https://github.com/onlooker-community/ecosystem/issues/178)) ([4f12a1e](https://github.com/onlooker-community/ecosystem/commit/4f12a1ee25300e0dc372542814e0922df5dbd335))
10
+
11
+ ## [0.13.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.13.0...librarian-v0.13.1) (2026-08-15)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **librarian:** make proposals the sole dedup source, and say so :broom: ([#156](https://github.com/onlooker-community/ecosystem/issues/156)) ([729d9ea](https://github.com/onlooker-community/ecosystem/commit/729d9ea3e328c2734712cfc5adf556c6798334aa))
17
+
18
+ ## [0.13.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.12.1...librarian-v0.13.0) (2026-08-15)
19
+
20
+
21
+ ### Features
22
+
23
+ * **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))
24
+
25
+ ## [0.12.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.12.0...librarian-v0.12.1) (2026-08-14)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **librarian:** close four follow-up defects from the promotion epic :broom: ([#148](https://github.com/onlooker-community/ecosystem/issues/148)) ([895df74](https://github.com/onlooker-community/ecosystem/commit/895df7430fa8ba75976d38964d061c21bff785d3))
31
+
32
+ ## [0.12.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.11.0...librarian-v0.12.0) (2026-08-13)
33
+
34
+
35
+ ### Features
36
+
37
+ * **librarian:** land judged lessons in the approved pool :package: ([#146](https://github.com/onlooker-community/ecosystem/issues/146)) ([f7adc72](https://github.com/onlooker-community/ecosystem/commit/f7adc7263a985d4cb7695b46d6f9dbab77dce587))
38
+
39
+ ## [0.11.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.10.0...librarian-v0.11.0) (2026-08-13)
40
+
41
+
42
+ ### Features
43
+
44
+ * **librarian:** derive an author identity that cannot be linked across scopes :closed_lock_with_key: ([#144](https://github.com/onlooker-community/ecosystem/issues/144)) ([29b3042](https://github.com/onlooker-community/ecosystem/commit/29b3042ef6cd6e7582206cfea6fcf37ebe661021))
45
+
46
+ ## [0.10.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.9.0...librarian-v0.10.0) (2026-08-12)
47
+
48
+
49
+ ### Features
50
+
51
+ * **librarian:** judge lessons before they leave the machine :balance_scale: ([#142](https://github.com/onlooker-community/ecosystem/issues/142)) ([a877527](https://github.com/onlooker-community/ecosystem/commit/a87752714961613b6b2bb768239c666f78d950f8))
52
+
53
+ ## [0.9.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.8.0...librarian-v0.9.0) (2026-08-11)
54
+
55
+
56
+ ### Features
57
+
58
+ * **librarian:** let a human take back a lesson confirmation :leftwards_arrow_with_hook: ([#139](https://github.com/onlooker-community/ecosystem/issues/139)) ([a4c4ac8](https://github.com/onlooker-community/ecosystem/commit/a4c4ac8ed5b79f5596f4e22abd1d23a8b0d3e834))
59
+
60
+ ## [0.8.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.7.1...librarian-v0.8.0) (2026-08-11)
61
+
62
+
63
+ ### Features
64
+
65
+ * **librarian:** let a human decide what leaves the machine :raised_hand: ([#137](https://github.com/onlooker-community/ecosystem/issues/137)) ([16d7673](https://github.com/onlooker-community/ecosystem/commit/16d76734833aee9c8818f84c1c1f704be4cdfc33))
66
+
3
67
  ## [0.7.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.7.0...librarian-v0.7.1) (2026-08-10)
4
68
 
5
69
 
@@ -18,7 +18,36 @@
18
18
  },
19
19
  "lesson_transform": {
20
20
  "model": "claude-haiku-4-5-20251001",
21
- "timeout_seconds": 20
21
+ "timeout_seconds": 20,
22
+ "total_budget_ms": 8000
23
+ },
24
+ "lesson_judging": {
25
+ "min_criterion_coverage": 0.6,
26
+ "rubrics": [
27
+ {
28
+ "id": "lesson-promotion",
29
+ "criteria": [
30
+ { "name": "grounding", "weight": 0.45, "min_pass": 0.7 },
31
+ { "name": "scope_accuracy", "weight": 0.35, "min_pass": 0.7 },
32
+ { "name": "generality", "weight": 0.2, "min_pass": 0.6 }
33
+ ],
34
+ "score_threshold": 0.75,
35
+ "judge_types": ["standard", "adversarial"],
36
+ "gate_policy": "majority"
37
+ },
38
+ {
39
+ "id": "lesson-promotion-public",
40
+ "criteria": [
41
+ { "name": "grounding", "weight": 0.32, "min_pass": 0.7 },
42
+ { "name": "scope_accuracy", "weight": 0.24, "min_pass": 0.7 },
43
+ { "name": "generality", "weight": 0.14, "min_pass": 0.6 },
44
+ { "name": "disclosure", "weight": 0.3, "min_pass": 0.9 }
45
+ ],
46
+ "score_threshold": 0.75,
47
+ "judge_types": ["standard", "adversarial"],
48
+ "gate_policy": "majority"
49
+ }
50
+ ]
22
51
  },
23
52
  "durability_filter": {
24
53
  "marker_phrases": [
@@ -0,0 +1,82 @@
1
+ # ADR-002: Agent definitions are shared assets; hooks are not
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ CLAUDE.md states: "Plugins communicate by emitting events to the JSONL log —
10
+ they do not call each other directly. All plugins depend on the ecosystem
11
+ substrate; no plugin depends on another plugin directly."
12
+
13
+ Lesson judging (`ecosystem-4z8.3`) needs a jury. Tribunal ships three judge
14
+ agent definitions and the rubric vocabulary. The stage couples librarian and
15
+ tribunal in some direction no matter how it is arranged: either librarian
16
+ reaches for tribunal's judges, or tribunal reaches into librarian's
17
+ project-keyed proposal files and writes verdicts back into them.
18
+
19
+ ## Decision
20
+
21
+ The invariant forbids **runtime** coupling — one plugin's hook or library
22
+ calling another's, which makes one plugin's failure another's. It does not
23
+ forbid reusing a **published agent definition** by name.
24
+
25
+ Librarian therefore owns the `judge` verb, both rubrics, the aggregate, and the
26
+ gate decision, and dispatches `tribunal-judge-standard` and
27
+ `tribunal-judge-adversarial` by name.
28
+
29
+ Librarian does **not** source any file under `plugins/tribunal/`. It implements
30
+ its own aggregate and gate — roughly twenty lines — rather than calling
31
+ `tribunal_aggregate` or `tribunal_gate_decide`.
32
+
33
+ ## Rationale
34
+
35
+ An agent definition is declarative: a markdown file with frontmatter and a
36
+ prompt. It has no runtime surface and cannot fail at call time in a way that
37
+ propagates — the harness resolves it, librarian never invokes tribunal's code
38
+ directly. That is a real difference from sourcing tribunal's bash, where a
39
+ signature change to `tribunal_gate_decide` breaks librarian mid-call and
40
+ neither plugin's tests would catch it.
41
+
42
+ It is a weaker guarantee than depending on a published schema, though.
43
+ `@onlooker-community/schema` is registered and drift-checked in CI (see
44
+ [ADR-005](../../../../docs/adr/005-runtime-emitter-fails-open.md)): a breaking
45
+ change to it fails a build before it ships. A judge agent's prompt carries no
46
+ such contract — nothing in either plugin's test suite stops a maintainer from
47
+ narrowing or repurposing what `tribunal-judge-standard` returns without
48
+ touching the agent's name. Calling that "closer to a published schema than to
49
+ calling another plugin's code" overstated the guarantee; only the *loud*
50
+ failure modes — the judge being renamed or removed — actually behave like a
51
+ schema break. See Consequences for what happens when the drift is quiet
52
+ instead.
53
+
54
+ Keeping the lifecycle in librarian also keeps `ecosystem-4z8.4`'s pool and
55
+ ledger in one plugin instead of splitting them across two.
56
+
57
+ ## Consequences
58
+
59
+ A judge agent renamed or removed in tribunal breaks lesson judging at dispatch
60
+ time. That is a visible, loud failure at the moment a human invokes the verb —
61
+ not a silent one — and the "could not judge" path already handles it: the
62
+ candidate stays `confirmed` and nothing is written.
63
+
64
+ A judge agent silently edited in place is a different, unmitigated risk. If a
65
+ maintainer changes what a judge returns without renaming it — dropping
66
+ `judge_type`, redefining what `feedback_summary` means, rescaling `score` —
67
+ nothing in this pipeline notices at edit time, because the agent definition
68
+ carries no drift check the way `@onlooker-community/schema` does. The
69
+ `usable` panel check in `librarian-lesson-judge.sh` only catches the subset of
70
+ that risk that changes the verdict's *shape*: it requires `judge_type` to be a
71
+ string, `score` a number, and `passed` a boolean, and returns UNJUDGED rather
72
+ than a false rejection if any is missing or mistyped. A *semantic* change —
73
+ same shape, different meaning, such as a score scale moving from 0–1 to
74
+ 0–100 — passes that check and is judged normally, with no signal to librarian
75
+ or its maintainers that the verdict no longer means what the aggregate and
76
+ gate assume it means. This ADR accepts that risk rather than closing it;
77
+ closing it would require either a real schema contract for judge output or
78
+ librarian validating semantics it does not own.
79
+
80
+ Librarian's gate logic can drift from tribunal's. Accepted deliberately: they
81
+ answer different questions. Tribunal gates an Actor's output with retry;
82
+ librarian gates a fixed artifact with none.
@@ -435,11 +435,32 @@ done
435
435
  #
436
436
  # Runs over the same durability survivors the classifier saw. Each artifact is
437
437
  # independent: a decline or an outage on one never stops the rest.
438
+ #
439
+ # Budgeted in aggregate, not just per call. Each transform carries a 20s
440
+ # ceiling of its own, but nothing bounded KEPT_COUNT of them end to end, so a
441
+ # backlog could hold SessionEnd open for minutes (ecosystem-qwi). The check is
442
+ # per iteration rather than once before the loop: a pre-loop gate only decides
443
+ # whether to start, and once started the cost is still unbounded — which is the
444
+ # gap the classifier loop above still has.
445
+ #
446
+ # Skipping is the safe direction. Untransformed artifacts are reconsidered on a
447
+ # later session, so the cost of stopping early is a delay; the cost of not
448
+ # stopping is a session that will not close.
438
449
  # ---------------------------------------------------------------------------
439
450
  LESSON_PROPOSED=0
440
451
  LESSON_DECLINED=0
452
+ LESSONS_SKIPPED=0
453
+
454
+ LESSON_BUDGET_MS=$(librarian_config_get '.librarian.lesson_transform.total_budget_ms' 2>/dev/null)
455
+ [[ -z "$LESSON_BUDGET_MS" || "$LESSON_BUDGET_MS" == "null" ]] && LESSON_BUDGET_MS=8000
456
+ LESSON_START_MS=$(librarian_now_ms)
441
457
 
442
458
  for ((li = 0; li < KEPT_COUNT; li++)); do
459
+ if [[ $(( $(librarian_now_ms) - LESSON_START_MS )) -ge "$LESSON_BUDGET_MS" ]]; then
460
+ LESSONS_SKIPPED=$(( KEPT_COUNT - li ))
461
+ break
462
+ fi
463
+
443
464
  LESSON_ARTIFACT=$(printf '%s' "$KEPT" | jq -c ".[$li]")
444
465
  [[ -z "$LESSON_ARTIFACT" || "$LESSON_ARTIFACT" == "null" ]] && continue
445
466
 
@@ -450,6 +471,11 @@ for ((li = 0; li < KEPT_COUNT; li++)); do
450
471
  esac
451
472
  done
452
473
 
474
+ # LESSONS_SKIPPED rides on scan.complete below rather than becoming an event of
475
+ # its own. A truncated stage 5 is not a truncated scan: the scan finishes
476
+ # normally and only this stage stops early, so the count belongs beside a
477
+ # healthy outcome rather than replacing it.
478
+
453
479
  # ----------------------------------------------------------------------------
454
480
  # Watermark advance + scan.complete.
455
481
  # ----------------------------------------------------------------------------
@@ -465,12 +491,14 @@ librarian_emit "librarian.scan.complete" "$SESSION_ID" "$(jq -cn \
465
491
  --arg outcome "$OUTCOME" \
466
492
  --argjson candidates_proposed "$PROPOSED_COUNT" \
467
493
  --argjson candidates_dropped "$TOTAL_DROPPED" \
494
+ --argjson lessons_skipped "$LESSONS_SKIPPED" \
468
495
  --argjson duration_ms "$DURATION_MS" \
469
496
  --argjson artifact_count_in_window "$ARTIFACT_COUNT" \
470
497
  '{
471
498
  outcome: $outcome,
472
499
  candidates_proposed: $candidates_proposed,
473
500
  candidates_dropped: $candidates_dropped,
501
+ lessons_skipped: $lessons_skipped,
474
502
  duration_ms: $duration_ms,
475
503
  artifact_count_in_window: $artifact_count_in_window
476
504
  }')"