@onlooker-community/ecosystem 0.43.6 → 0.44.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecosystem",
3
- "version": "0.43.6",
3
+ "version": "0.44.0",
4
4
  "description": "Observability substrate for Claude Code. Provides the shared $ONLOOKER_DIR storage root (default $HOME/.onlooker), canonical schema-validated event emission, session and tool tracking hooks, and prompt rules. Required by all other Onlooker plugins.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,15 +1,15 @@
1
1
  {
2
- ".": "0.43.6",
2
+ ".": "0.44.0",
3
3
  "plugins/archivist": "0.3.2",
4
4
  "plugins/tribunal": "1.2.8",
5
5
  "plugins/echo": "0.3.2",
6
- "plugins/cartographer": "0.6.1",
6
+ "plugins/cartographer": "0.6.2",
7
7
  "plugins/governor": "0.3.3",
8
8
  "plugins/compass": "0.4.2",
9
9
  "plugins/scribe": "0.4.3",
10
10
  "plugins/counsel": "0.5.2",
11
11
  "plugins/warden": "0.3.2",
12
- "plugins/librarian": "0.13.3",
12
+ "plugins/librarian": "0.14.0",
13
13
  "plugins/curator": "0.2.4",
14
14
  "plugins/historian": "0.3.4",
15
15
  "plugins/assayer": "1.1.4",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.44.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.7...ecosystem-v0.44.0) (2026-08-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * **librarian:** let lessons status close the judge walk :abacus: ([#205](https://github.com/onlooker-community/ecosystem/issues/205)) ([8da1cd7](https://github.com/onlooker-community/ecosystem/commit/8da1cd72a7e564aae44750efaa1e59f1ecd59641))
9
+
10
+ ## [0.43.7](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.6...ecosystem-v0.43.7) (2026-08-23)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **cartographer:** drop a typeless finding instead of hashing it as "unknown" :broom: ([#203](https://github.com/onlooker-community/ecosystem/issues/203)) ([a949d5d](https://github.com/onlooker-community/ecosystem/commit/a949d5d43f3ae3515ac6278b35d1be5cc468cb0e))
16
+
3
17
  ## [0.43.6](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.5...ecosystem-v0.43.6) (2026-08-22)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.43.6",
3
+ "version": "0.44.0",
4
4
  "description": "Agents, skills, hooks, commands, rules, and MCP configurations that power [Onlooker](https://onlooker.dev)",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cartographer",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Proactive periodic auditor of the persistent instruction layer (CLAUDE.md, AGENTS.md, .claude/rules/). Discovers all instruction files in the repo, extracts semantic maps, and surfaces contradictions, shadowing, gaps, and drift before they cause expensive agent misbehavior. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to the Cartographer plugin are documented here.
4
4
 
5
+ ## [0.6.2](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.6.1...cartographer-v0.6.2) (2026-08-23)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **cartographer:** drop a typeless finding instead of hashing it as "unknown" :broom: ([#203](https://github.com/onlooker-community/ecosystem/issues/203)) ([a949d5d](https://github.com/onlooker-community/ecosystem/commit/a949d5d43f3ae3515ac6278b35d1be5cc468cb0e))
11
+
5
12
  ## [0.6.1](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.6.0...cartographer-v0.6.1) (2026-08-22)
6
13
 
7
14
 
@@ -247,7 +247,29 @@ run_synthesize() {
247
247
  while IFS= read -r finding; do
248
248
  [[ -z "$finding" ]] && continue
249
249
  local ftype ffile_a fexcerpt_a ffile_b fexcerpt_b
250
- ftype=$(printf '%s' "$finding" | jq -r '.type // "unknown"')
250
+ # `// ""` collapses absent, null and empty into one check, the same way
251
+ # cartographer_issue_found_payload does — jq's `//` treats "" as
252
+ # present, so an empty type would otherwise slip past.
253
+ ftype=$(printf '%s' "$finding" | jq -r '.type // ""')
254
+
255
+ # A finding with no type is malformed: every analyzer writes a literal
256
+ # type, so this means the phase that produced it has a bug. Drop it here
257
+ # rather than hashing it.
258
+ #
259
+ # This used to default to "unknown". The emit path already refused to
260
+ # announce such a finding (ecosystem-ci0), but the orchestrator stored
261
+ # it anyway under a hash derived from a type it does not have, and then
262
+ # touched its dedup sentinel. That sentinel made the silence permanent:
263
+ # every later audit saw "known", took the refresh path, and emitted
264
+ # nothing — so the finding sat on disk forever, counted in
265
+ # new_finding_count, and could never be announced. Dropping it keeps the
266
+ # stored corpus honest and leaves the hash of every well-formed finding
267
+ # untouched, so no existing dedup sentinel is orphaned (ecosystem-0ty).
268
+ if [[ -z "$ftype" ]]; then
269
+ log "synthesize: dropped a finding that carries no type — the analyzer that produced it has a bug"
270
+ continue
271
+ fi
272
+
251
273
  ffile_a=$(printf '%s' "$finding" | jq -r '.file_a // ""')
252
274
  fexcerpt_a=$(printf '%s' "$finding" | jq -r '.excerpt_a // ""')
253
275
  ffile_b=$(printf '%s' "$finding" | jq -r '.file_b // ""')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librarian",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.13.3...librarian-v0.14.0) (2026-08-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * **librarian:** let lessons status close the judge walk :abacus: ([#205](https://github.com/onlooker-community/ecosystem/issues/205)) ([8da1cd7](https://github.com/onlooker-community/ecosystem/commit/8da1cd72a7e564aae44750efaa1e59f1ecd59641))
9
+
3
10
  ## [0.13.3](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.13.2...librarian-v0.13.3) (2026-08-22)
4
11
 
5
12
 
@@ -641,13 +641,44 @@ librarian_cli_lessons_defer() {
641
641
  printf 'Deferred %s; it stays in the queue.\n' "$lesson_id"
642
642
  }
643
643
 
644
+ # One authoritative line for the whole lesson queue.
645
+ #
646
+ # Reports a count per status rather than pending alone, because the judge walk
647
+ # closes on this call. It used to close on counts the model had tallied itself
648
+ # across the loop, with nothing to check them against — every other walk in the
649
+ # skill ends on a CLI call, and this one had none to end on (ecosystem-sv3).
650
+ #
651
+ # `awaiting promotion` is the walk's fourth bucket: a lesson whose verdict is
652
+ # recorded but whose pool entry or declined row has not landed. promote()
653
+ # stamps promoted_at LAST, after the terminal record, so a terminal status with
654
+ # no stamp is exactly that state — and it must not be folded into `approved`,
655
+ # which would claim a pool entry that does not exist.
656
+ #
657
+ # The walk's remaining bucket, could-not-judge, is deliberately absent: such a
658
+ # candidate stays `confirmed`, which is indistinguishable on disk from one that
659
+ # was never judged this run. That count is run-scoped and stays the model's.
644
660
  librarian_cli_lessons_status() {
645
661
  local cwd="${1:-}"
646
- local key pending
662
+ local key
647
663
  key=$(_librarian_cli_project_key "$cwd")
648
664
  [[ -z "$key" ]] && { printf 'No project key resolvable from this directory.\n'; return 1; }
649
- pending=$(librarian_lesson_list_pending "$key")
650
- printf 'lessons pending: %s\n' "$(printf '%s' "$pending" | jq 'length')"
665
+
666
+ local s counts=()
667
+ for s in pending confirmed approved rejected; do
668
+ counts+=("$(librarian_lesson_list_by_status "$key" "$s" | jq 'length')")
669
+ done
670
+
671
+ # A terminal verdict whose promote() never stamped. Counted from the same
672
+ # per-status reads rather than a second directory walk.
673
+ local awaiting=0 n
674
+ for s in approved rejected; do
675
+ n=$(librarian_lesson_list_by_status "$key" "$s" \
676
+ | jq '[.[] | select(has("promoted_at") | not)] | length')
677
+ awaiting=$(( awaiting + n ))
678
+ done
679
+
680
+ printf 'lessons pending: %s, confirmed: %s, approved: %s, rejected: %s, awaiting promotion: %s\n' \
681
+ "${counts[0]}" "${counts[1]}" "${counts[2]}" "${counts[3]}" "$awaiting"
651
682
  }
652
683
 
653
684
  librarian_cli_lessons() {
@@ -141,10 +141,21 @@ For `lessons judge`, run the jury over confirmed candidates:
141
141
  those ids and list them at the end so the user knows to re-run. A broken judge
142
142
  must never become a rejection — the artifact's watermark has already moved,
143
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.
144
+ 5. **Finish on `librarian_cli lessons status`, not on your own tally.** It
145
+ prints one line pending, confirmed, approved, rejected, and **awaiting
146
+ promotion** read from disk, so it is the same kind of authoritative close
147
+ the review walk ends on. Report those numbers as it gives them; a long batch
148
+ is exactly where a running count kept in your head drifts.
149
+
150
+ Two things the call cannot tell you, which you must still report yourself:
151
+
152
+ - **could-not-judge** — those candidates stay `confirmed`, which on disk is
153
+ indistinguishable from a candidate that was never judged this run. Keep
154
+ the ids from step 4 and list them.
155
+ - **which** lessons are awaiting promotion. The call gives the count; you
156
+ have the ids. List them with `librarian_cli lessons promote <id>` as the
157
+ next step, and don't fold them into "approved" — a lesson here has no pool
158
+ entry yet. If your ids and the count disagree, trust the count and say so.
148
159
 
149
160
  `scope_accuracy` is the criterion that matters most on a `version_independent`
150
161
  candidate. The schema guarantees such a lesson **carries** a justification; this
@@ -268,10 +268,12 @@ _run_audit() {
268
268
  [ "$output" = "session_start_first_run" ]
269
269
  }
270
270
 
271
- # A typeless finding must not vanish. The builder refuses it (ecosystem-ci0),
272
- # but refusing is only half the fix: if the orchestrator discards that refusal
273
- # the event is still silently absent, one layer further up. The audit log is
274
- # where an operator would go looking for it.
271
+ # A typeless finding must not vanish. Since ecosystem-0ty the synthesize phase
272
+ # drops it before it is hashed, so this line now comes from there rather than
273
+ # from the emit-phase builder refusal that ecosystem-ci0 added. Both write
274
+ # "carries no type" on purpose: what matters to an operator is that the audit
275
+ # log says a finding was discarded and why, not which layer discarded it. The
276
+ # builder's own refusal is still covered directly in cartographer-events.bats.
275
277
  @test "a finding with no type is reported in the audit log" {
276
278
  cat > "${STUB_BIN}/claude" <<'STUB'
277
279
  #!/usr/bin/env bash
@@ -286,6 +288,50 @@ STUB
286
288
  grep -q 'carries no type' "$AUDIT_LOG"
287
289
  }
288
290
 
291
+ # Refusing to announce a typeless finding was only half of ecosystem-ci0. The
292
+ # orchestrator still stored it under a hash computed from a type it does not
293
+ # have, then touched its dedup sentinel — so on every later audit the sentinel
294
+ # said "known", the refresh path ran, and no bus event ever fired again. The
295
+ # finding sat on disk forever, counted in new_finding_count, and was
296
+ # permanently unannounceable. These three assert it is dropped instead
297
+ # (ecosystem-0ty).
298
+
299
+ _typeless_stub() {
300
+ cat > "${STUB_BIN}/claude" <<'STUB'
301
+ #!/usr/bin/env bash
302
+ cat >/dev/null
303
+ echo call >> "$CLAUDE_CALL_LOG"
304
+ printf '[{"severity":"warning","file_a":"CLAUDE.md","excerpt_a":"Always prefer tabs.","description":"typeless"}]'
305
+ STUB
306
+ chmod +x "${STUB_BIN}/claude"
307
+ }
308
+
309
+ @test "a finding with no type is never written to findings/" {
310
+ _typeless_stub
311
+ run _run_audit
312
+ [ "$status" -eq 0 ] || return 1
313
+ [ "$(find "${CARTOGRAPHER_DIR}/findings" -name '*.json' 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
314
+ }
315
+
316
+ @test "a finding with no type leaves no dedup sentinel" {
317
+ # The sentinel is what made this permanent: with one on disk, a corrected
318
+ # finding that later hashes the same way would be seen as already known and
319
+ # silently refreshed instead of announced.
320
+ _typeless_stub
321
+ run _run_audit
322
+ [ "$status" -eq 0 ] || return 1
323
+ [ "$(find "${CARTOGRAPHER_DIR}/dedup" -type f 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
324
+ }
325
+
326
+ @test "a finding with no type is not counted as a new finding" {
327
+ # audit.complete used to report a finding that produced no issue.found,
328
+ # so a consumer reading only the bus saw counts that its own event stream
329
+ # could not account for.
330
+ _typeless_stub
331
+ _run_audit
332
+ _last_audit_complete | jq -e '.payload.new_finding_count == 0' >/dev/null
333
+ }
334
+
289
335
 
290
336
  # ── Resolution reaching the bus (ecosystem-w2i) ──────────────────────────────
291
337
  #
@@ -387,6 +387,29 @@ _cli_setup() {
387
387
  source "${PLUGIN_ROOT}/scripts/lib/librarian-cli.sh"
388
388
  }
389
389
 
390
+ # ── lessons status: a count per status (ecosystem-sv3) ───────────────────────
391
+ #
392
+ # The judge walk used to close by reporting counts the model tracked itself
393
+ # across the loop, with nothing to check them against — every other walk in the
394
+ # skill ends on an authoritative CLI call. `lessons status` reported only a
395
+ # pending count, so there was nothing to ground the summary on.
396
+
397
+ # Force a proposal to a terminal status on disk. Reaching `approved` through
398
+ # the real path means empaneling a jury, which is what the judge walk exists to
399
+ # avoid doing twice; the status field is what the counter reads.
400
+ _set_status() {
401
+ local id="$1" status="$2" promoted="${3:-}"
402
+ local f
403
+ f="$(librarian_lessons_dir "$PROJECT_KEY")/proposals/${id}.json"
404
+ local tmp="${f}.tmp"
405
+ if [[ -n "$promoted" ]]; then
406
+ jq --arg s "$status" --arg p "$promoted" '.status = $s | .promoted_at = $p' "$f" >"$tmp"
407
+ else
408
+ jq --arg s "$status" '.status = $s | del(.promoted_at)' "$f" >"$tmp"
409
+ fi
410
+ mv -f "$tmp" "$f"
411
+ }
412
+
390
413
  @test "lessons status reports zero on an empty queue" {
391
414
  _cli_setup
392
415
  run librarian_cli lessons status "$PROJECT_REPO"
@@ -394,6 +417,72 @@ _cli_setup() {
394
417
  [[ "$output" == *"0"* ]] || return 1
395
418
  }
396
419
 
420
+ @test "lessons status reports a count for every status, not just pending" {
421
+ _cli_setup
422
+ local a b c d
423
+ a=$(_seed_pending)
424
+ b=$(_seed_pending)
425
+ c=$(_seed_pending)
426
+ d=$(_seed_pending)
427
+ librarian_lesson_confirm "$PROJECT_KEY" "$b" "org"
428
+ _set_status "$c" "approved" "2026-08-01T00:00:00Z"
429
+ _set_status "$d" "rejected" "2026-08-01T00:00:00Z"
430
+
431
+ run librarian_cli lessons status "$PROJECT_REPO"
432
+ [ "$status" -eq 0 ] || return 1
433
+ [[ "$output" == *"pending: 1"* ]] || return 1
434
+ [[ "$output" == *"confirmed: 1"* ]] || return 1
435
+ [[ "$output" == *"approved: 1"* ]] || return 1
436
+ [[ "$output" == *"rejected: 1"* ]]
437
+ }
438
+
439
+ @test "lessons status counts a judged-but-unpromoted lesson separately" {
440
+ # The judge walk's fourth bucket. promote() stamps promoted_at LAST, after
441
+ # the terminal record lands, so a terminal status with no stamp is exactly
442
+ # the state the walk is told not to fold into "approved" — the lesson has
443
+ # no pool entry yet.
444
+ _cli_setup
445
+ local a b
446
+ a=$(_seed_pending)
447
+ b=$(_seed_pending)
448
+ _set_status "$a" "approved" "2026-08-01T00:00:00Z"
449
+ _set_status "$b" "approved"
450
+
451
+ run librarian_cli lessons status "$PROJECT_REPO"
452
+ [ "$status" -eq 0 ] || return 1
453
+ [[ "$output" == *"approved: 2"* ]] || return 1
454
+ [[ "$output" == *"awaiting promotion: 1"* ]]
455
+ }
456
+
457
+ @test "lessons status reports awaiting promotion for a rejected lesson too" {
458
+ # A rejected verdict also lands its terminal record (the declined row)
459
+ # before the stamp, so the same recovery state exists on that side.
460
+ _cli_setup
461
+ local a b
462
+ a=$(_seed_pending)
463
+ b=$(_seed_pending)
464
+ _set_status "$a" "rejected"
465
+ # A promoted sibling, so the count cannot come out right by counting every
466
+ # rejected lesson — without it this passes whether or not the stamp is read.
467
+ _set_status "$b" "rejected" "2026-08-01T00:00:00Z"
468
+
469
+ run librarian_cli lessons status "$PROJECT_REPO"
470
+ [ "$status" -eq 0 ] || return 1
471
+ [[ "$output" == *"rejected: 2"* ]] || return 1
472
+ [[ "$output" == *"awaiting promotion: 1"* ]]
473
+ }
474
+
475
+ @test "lessons status reports zeros across every status on an empty queue" {
476
+ _cli_setup
477
+ run librarian_cli lessons status "$PROJECT_REPO"
478
+ [ "$status" -eq 0 ] || return 1
479
+ [[ "$output" == *"pending: 0"* ]] || return 1
480
+ [[ "$output" == *"confirmed: 0"* ]] || return 1
481
+ [[ "$output" == *"approved: 0"* ]] || return 1
482
+ [[ "$output" == *"rejected: 0"* ]] || return 1
483
+ [[ "$output" == *"awaiting promotion: 0"* ]]
484
+ }
485
+
397
486
  @test "lessons list shows a pending lesson" {
398
487
  _cli_setup
399
488
  id=$(_seed_pending)