@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
@@ -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": "curator",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Maintenance layer for the user's typed auto-memory store. At every SessionStart, runs four cheap heuristic checks (date_decayed, path_broken, broken_index, orphaned_memory) against the memories at ~/.claude/projects/<encoded>/memory/ inside a wall-clock budget. Surfaces findings as a one-line pointer to /curator review; never edits the memory store directly. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.3](https://github.com/onlooker-community/ecosystem/compare/curator-v0.2.2...curator-v0.2.3) (2026-08-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * make the local bats suite tell the truth :mag: ([#135](https://github.com/onlooker-community/ecosystem/issues/135)) ([f0763e0](https://github.com/onlooker-community/ecosystem/commit/f0763e09f3caf2d39c89f28befd12567af0af845))
9
+
3
10
  ## [0.2.2](https://github.com/onlooker-community/ecosystem/compare/curator-v0.2.1...curator-v0.2.2) (2026-08-02)
4
11
 
5
12
 
@@ -39,7 +39,8 @@ _CURATOR_EVENT_JS="${_CURATOR_EVENT_JS:-$(_curator_resolve_event_js)}"
39
39
  curator_emit() {
40
40
  local event_type="${1:-}"
41
41
  local session_id="${2:-}"
42
- local payload="${3:-{\}}"
42
+ local payload="${3:-}"
43
+ [ -z "$payload" ] && payload='{}'
43
44
 
44
45
  [[ -z "$event_type" || -z "$session_id" ]] && return 0
45
46
  [[ -z "$_CURATOR_EVENT_JS" || ! -f "$_CURATOR_EVENT_JS" ]] && return 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "historian",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Episodic memory layer. At SessionEnd, chunks and sanitizes the session transcript and stores chunks under $ONLOOKER_DIR/historian/<project-key>/sessions/ (default $HOME/.onlooker). On UserPromptSubmit, embeds the prompt and performs similarity retrieval over stored chunks to surface relevant past context. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.3](https://github.com/onlooker-community/ecosystem/compare/historian-v0.3.2...historian-v0.3.3) (2026-08-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * make the local bats suite tell the truth :mag: ([#135](https://github.com/onlooker-community/ecosystem/issues/135)) ([f0763e0](https://github.com/onlooker-community/ecosystem/commit/f0763e09f3caf2d39c89f28befd12567af0af845))
9
+
3
10
  ## [0.3.2](https://github.com/onlooker-community/ecosystem/compare/historian-v0.3.1...historian-v0.3.2) (2026-08-01)
4
11
 
5
12
 
@@ -39,7 +39,8 @@ _HISTORIAN_EVENT_JS="${_HISTORIAN_EVENT_JS:-$(_historian_resolve_event_js)}"
39
39
  historian_emit() {
40
40
  local event_type="${1:-}"
41
41
  local session_id="${2:-}"
42
- local payload="${3:-{\}}"
42
+ local payload="${3:-}"
43
+ [ -z "$payload" ] && payload='{}'
43
44
 
44
45
  [[ -z "$event_type" || -z "$session_id" ]] && return 0
45
46
  [[ -z "$_HISTORIAN_EVENT_JS" || ! -f "$_HISTORIAN_EVENT_JS" ]] && return 0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librarian",
3
- "version": "0.7.0",
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,76 @@
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
+
67
+ ## [0.7.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.7.0...librarian-v0.7.1) (2026-08-10)
68
+
69
+
70
+ ### Bug Fixes
71
+
72
+ * 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))
73
+
3
74
  ## [0.7.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.6.1...librarian-v0.7.0) (2026-08-09)
4
75
 
5
76
 
@@ -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": [