@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
@@ -0,0 +1,254 @@
1
+ # Per-Criterion Scores — Design
2
+
3
+ **Status:** Approved, not started.
4
+ **Tracked by:** `ecosystem-pht`.
5
+ **Spans two repositories.** The payload lives in `@onlooker-community/schema`;
6
+ the consumers live here.
7
+
8
+ ---
9
+
10
+ ## What this is
11
+
12
+ Tribunal's rubrics declare a `weight` and a `min_pass` per criterion, and
13
+ `tribunal-rubric.sh` validates both are numbers in `[0,1]`. **Neither is ever
14
+ applied.**
15
+
16
+ `tribunal_aggregate` takes the rubric as its third parameter and explicitly
17
+ discards it (`: "$_rubric"`); `weighted_mean` falls through to the same jq
18
+ expression as `mean`, a plain average of each judge's single overall score.
19
+ `min_pass` is enforced nowhere at all, because `TribunalVerdictPayload` carries
20
+ one scalar `score`, one boolean `passed`, and `criteria_evaluated` — a list of
21
+ criterion *names* with no scores attached. The orchestrator never learns what a
22
+ judge scored on any individual criterion.
23
+
24
+ So a rubric cannot express "this one criterion is a floor." A judge scoring 0.3
25
+ on a safety-critical criterion and 0.95 on everything else can still report
26
+ `passed: true`, and under `gate_policy: majority` a single dissenter is
27
+ outvoted regardless.
28
+
29
+ ## Why it matters now
30
+
31
+ `ecosystem-4z8.3` shipped a public tier for lesson promotion whose entire
32
+ safety argument was a disclosure floor at `min_pass` 0.9 — a near-veto, on the
33
+ reasoning that correctness rots and `applies_to` retires it, but harm does not:
34
+ a leaked credential never expires on its own.
35
+
36
+ That floor could not be built, so the public tier ships `gate_policy:
37
+ unanimous` instead — a deliberate stand-in intended to deliver the property
38
+ that mattered ("a single judge's objection cannot be outvoted"), though not
39
+ disclosure-specific.
40
+
41
+ **The stand-in does not work.** Both librarian rubrics declare `judge_types:
42
+ ["standard", "adversarial"]`, a panel of two, and at panel size two
43
+ `unanimous` (`passed == count`) and `majority` (`passed * 2 > count`) return
44
+ the same answer for every possible pass count — 0, 1, or 2. They diverge only
45
+ at three judges or more, and `librarian_lesson_judge`'s `usable` check refuses
46
+ any panel whose judge-type multiset does not exactly match the rubric's, so a
47
+ third judge never reaches the gate. **The public tier has never been stricter
48
+ than the org tier.**
49
+
50
+ Two individually-correct changes combined to produce an inert one. Tracked as
51
+ `ecosystem-j74`.
52
+
53
+ That makes this design load-bearing rather than an improvement: the public
54
+ tier has no extra protection today, and the floor below is what gives it one.
55
+ The `4z8.3` spec and the `lesson-promotion-public` rubric comment both still
56
+ describe `unanimous` as a working guarantee and must be corrected.
57
+
58
+ ## The payload
59
+
60
+ Add `criterion_scores` to `TribunalVerdictPayload`, alongside the existing
61
+ `criteria_evaluated`:
62
+
63
+ ```jsonc
64
+ "criterion_scores": {
65
+ "type": "object",
66
+ "additionalProperties": { "type": "number", "minimum": 0, "maximum": 1 }
67
+ }
68
+ ```
69
+
70
+ ### Optional, not required
71
+
72
+ Three shipped plugins emit this payload, and the runtime emitter validates
73
+ whenever the schema package is resolvable. Making the field required would
74
+ invalidate every existing producer the moment the version bumps.
75
+
76
+ Optional also matches reality during the rollout: the ecosystem consumes the
77
+ new version before its judge agents emit the field, so consumers must handle
78
+ absence anyway.
79
+
80
+ ### A map, not a parallel array
81
+
82
+ `criteria_evaluated` is already a name list. A scores array beside it would
83
+ have to stay index-aligned, with nothing enforcing that — and a
84
+ silently-misaligned pair attributes each score to the wrong criterion, which is
85
+ worse than having no scores.
86
+
87
+ A map makes the association explicit and lets the aggregator look up by the
88
+ rubric's own criterion name.
89
+
90
+ ### `additionalProperties`, not fixed keys
91
+
92
+ Criterion names come from a rubric, and rubrics are user-extensible per
93
+ ADR-004. Librarian's `lesson-promotion` already defines `grounding`,
94
+ `scope_accuracy`, `generality`, and `disclosure` — none of which appear in
95
+ tribunal's default rubric. Enumerating keys in the schema would make every new
96
+ rubric a schema change.
97
+
98
+ ### `criteria_evaluated` and `criterion_scores` may disagree
99
+
100
+ Deliberately not constrained. A judge may evaluate a criterion it cannot score,
101
+ or score one it did not list. Cross-field consistency belongs at ingest, not in
102
+ a JSON Schema — the same reasoning the lesson contract gives for leaving
103
+ `agreed <= judges` out of `ZConsensus`.
104
+
105
+ ### The block needs a reason of its own
106
+
107
+ `tribunal.gate.blocked` carries a closed `reason` enum — `low_score`,
108
+ `meta_override`, `bias_detected`, `dissent_unresolved` — under
109
+ `additionalProperties: false`. None of them describes a criterion floor, and
110
+ the whole premise of the floor is a block that fires *while the aggregate
111
+ clears `score_threshold`*. Emitting `low_score` would be false, and
112
+ indistinguishable from a genuine threshold miss.
113
+
114
+ So the same release adds `criterion_floor` to the enum plus an optional
115
+ `failed_criterion` string. It ships here rather than with the consumers
116
+ because the event log is append-only: every floor block emitted before the
117
+ enum existed would be permanently recorded as a low-score block.
118
+
119
+ Librarian's own gate needs no schema change for this — its verdict reason is
120
+ written to on-disk proposal JSON, not to a schema-validated event.
121
+
122
+ ### Both files change together
123
+
124
+ `src/types.ts` is **hand-written**; `schemas/payload/plugins-safety.json` is the
125
+ runtime contract. `scripts/generate-types.js` cross-checks them, but its own
126
+ header says divergence is currently a warning rather than a hard failure
127
+ (`TODO(ONL-6 hard fail)`). Nothing will stop the two from drifting, so they must
128
+ be edited in step and the test fixture must exercise the new field.
129
+
130
+ ## The consumers
131
+
132
+ Second PR, after the schema version publishes.
133
+
134
+ ### Judges emit it
135
+
136
+ `tribunal-judge-standard` already receives the rubric with its criteria and is
137
+ told to "score each criterion in [0,1]". It has nowhere to put the result; its
138
+ output contract gains `criterion_scores`.
139
+
140
+ **Neither `tribunal-judge-adversarial` nor `tribunal-judge-security` has a
141
+ rubric section at all.** Both report `criteria_evaluated` lists drawn from their
142
+ own investigative lenses — `edge-cases`, `concurrency`, `idempotency` for one;
143
+ `injection`, `secrets`, `path-traversal` for the other — disjoint from every
144
+ rubric in the repo.
145
+
146
+ So the adversarial agent needs a rubric section before any floor can be
147
+ trusted, and the consumer plan must state which judge types are expected to
148
+ score which criteria. A floor on a criterion no judge scores is not a floor.
149
+
150
+ ### `weighted_mean` becomes real
151
+
152
+ `tribunal_aggregate` already receives the rubric it discards. Weight each
153
+ criterion, then average judges per criterion.
154
+
155
+ **Normalize by the sum of weights rather than assuming 1.0.** Tribunal's own
156
+ `tribunal-rubric.sh` does validate the total, rejecting any rubric outside
157
+ 0.99–1.01 — so a mis-summed rubric cannot reach the aggregator through *that*
158
+ path. But librarian does not use it: `librarian_lesson_rubric_get` performs no
159
+ validation at all, and a librarian rubric summing to 1.30 would silently
160
+ mis-score. Normalizing costs one division and removes the difference between
161
+ the two paths.
162
+
163
+ **Degrade to `mean` when scores are absent**, which is the state of every
164
+ verdict emitted before the judges are updated. Silently producing 0 for a
165
+ missing criterion would turn an un-upgraded judge into a blocking one.
166
+
167
+ **Absent must stay distinguishable from zero, in the code as well as the
168
+ contract.** `jq`'s `// 0` idiom is reflexive throughout these libraries and
169
+ collapses exactly that distinction — a judge that did not score a criterion
170
+ would read as a judge that scored it 0.0, the failure mode this whole section
171
+ exists to avoid. Use `has()` or an explicit `null` test.
172
+
173
+ **Look criterion names up with `--arg`, never on a dotted path.** Hyphenated
174
+ names are already in shipped agent contracts (`path-traversal`, `edge-cases`),
175
+ and `jq '.criterion_scores.path-traversal'` is a *compile* error — `jq` exits 3
176
+ with empty stdout, which `awk` then reads as 0. That fails toward blocking, on
177
+ a name the rubric author is entitled to write. `.criterion_scores[$name]` with
178
+ `--arg name` is correct for every name.
179
+
180
+ ### Librarian needs signature changes the bead did not budget for
181
+
182
+ Librarian does not call either tribunal function named above. Its
183
+ `librarian_lesson_aggregate` takes **no rubric parameter** and hardcodes a
184
+ plain mean; `librarian_lesson_gate` receives no criteria either. Both need
185
+ their signatures widened before the public tier's floor can exist, and the
186
+ consumer plan must schedule that work rather than assuming tribunal's
187
+ aggregator is shared.
188
+
189
+ ### `min_pass` becomes enforceable
190
+
191
+ `tribunal_gate_decide` blocks when any criterion scores below its floor,
192
+ regardless of the aggregate or the gate policy. That is the whole point: a
193
+ floor a strong weighted mean cannot average away.
194
+
195
+ **Absent scores must not block.** A verdict with no `criterion_scores` cannot
196
+ violate a floor it never reported, and treating absence as violation would make
197
+ every pre-upgrade judge fail every rubric with a floor.
198
+
199
+ ### The public tier gets its real floor
200
+
201
+ Librarian's `lesson-promotion-public` swaps `gate_policy: unanimous` back to
202
+ `majority`, relying on `disclosure`'s `min_pass: 0.9` to do the blocking.
203
+
204
+ This is the point where the public tier first becomes stricter than the org
205
+ tier in fact rather than in intent — see `ecosystem-j74`. The swap is
206
+ therefore not a simplification, and the decision was to let this work supply
207
+ the protection rather than widen the panel now and pay for a third Opus judge
208
+ on every public candidate.
209
+
210
+ The `4z8.3` spec records `unanimous` as an explicit stand-in and says "when
211
+ `pht` lands, this can narrow to a true per-criterion floor." That spec, and the
212
+ librarian rubric comment describing the weights as inert, both need updating —
213
+ and the correction is not only that the stand-in was replaced, but that it
214
+ never had the effect it claimed.
215
+
216
+ ## Testing
217
+
218
+ Schema repo: the existing fixture-based `validate.test.ts`. A verdict carrying
219
+ `criterion_scores` validates; one carrying a score outside `[0,1]` or a non-number
220
+ value does not; one omitting the field entirely still validates, because that is
221
+ every producer today. A gate.blocked with `reason: "criterion_floor"` validates
222
+ with and without `failed_criterion`; an unrecognized reason is still rejected,
223
+ and that test must fail because of the enum rather than
224
+ `additionalProperties`.
225
+
226
+ Ecosystem: bats, isolated temp home.
227
+
228
+ - **`weighted_mean` produces a different result from `mean` when weights are
229
+ unequal.** Today they are identical, so any test that cannot tell them apart
230
+ proves nothing.
231
+ - Weights summing to something other than 1.0 normalize correctly — driven
232
+ through **librarian's** rubric loader, since tribunal's own validator rejects
233
+ such a rubric before the aggregator ever sees it.
234
+ - A criterion below its `min_pass` blocks even when the aggregate clears
235
+ `score_threshold` **and** the gate policy is satisfied — the property that
236
+ does not exist today.
237
+ - A verdict with no `criterion_scores` neither blocks nor scores 0; aggregation
238
+ degrades to `mean`. Distinctly: a verdict scoring a criterion **at** 0.0
239
+ *does* block, proving absence and zero are not conflated.
240
+ - A hyphenated criterion name scores and gates correctly.
241
+ - A rubric whose floor names a criterion **no judge scored** is surfaced rather
242
+ than silently passing — the adversarial-judge gap above, made visible.
243
+ - The public rubric, after the swap, blocks a lesson on a low `disclosure` score
244
+ while a low `generality` score no longer blocks it. Note this is a difference
245
+ from *intended* `unanimous` behavior, not from shipped behavior: at the
246
+ configured panel of two, `unanimous` and `majority` are the same gate, so
247
+ today's public tier blocks on neither.
248
+
249
+ ## Out of scope
250
+
251
+ Retiring `criteria_evaluated`, which stays as-is. Any new judge type. The
252
+ `aggregation_method` enum, which already carries `weighted_mean`. And the
253
+ `TODO(ONL-6)` hard-fail on type/schema divergence — worth doing, but it is the
254
+ schema repo's own cleanup, not this thread's.
@@ -0,0 +1,249 @@
1
+ # Cartographer `undocumented_entity` Phase — Design
2
+
3
+ **Status:** Approved, not started.
4
+ **Tracked by:** `ecosystem-3eu`.
5
+ **Single repository.** Everything lands under `plugins/cartographer/`.
6
+
7
+ ---
8
+
9
+ ## What this is
10
+
11
+ Cartographer's four analysis phases all start from the *text* of the instruction
12
+ files. `contradiction` and `dead_rule` compare rules against each other,
13
+ `scope_collision` compares project rules against global ones, and `stale_ref`
14
+ extracts path-like tokens out of the instruction files and tests them on the
15
+ filesystem (`cartographer-analyze.sh:99-125`).
16
+
17
+ Every phase walks doc → disk. Nothing walks disk → doc.
18
+
19
+ So something that exists on disk and is simply not mentioned is structurally
20
+ invisible: it produces no token to extract, no contradiction, no dead rule, and
21
+ no scope collision. This is why cartographer never flagged that `CLAUDE.md`'s
22
+ plugin map documented 12 of 17 plugins, omitting `librarian` — the plugin that
23
+ owns the entire upstream chain the lesson-promotion pipeline hangs off. Found
24
+ while fixing `ecosystem-yp8`.
25
+
26
+ This is a coverage gap, not a defect. An omission is a different class of drift
27
+ from a stale reference, and cartographer was only ever built to catch the
28
+ latter.
29
+
30
+ ## Why an absence is not automatically a defect
31
+
32
+ The obvious framing — "enumerate everything on disk, flag whatever no
33
+ instruction file mentions" — produces noise, because most of a repository has no
34
+ business being named in `CLAUDE.md`.
35
+
36
+ `docs/adr/` is the clean counterexample. Five ADRs sit on disk; `CLAUDE.md` names
37
+ ADR-001, ADR-004, and ADR-005 and says nothing about 002 or 003. That is correct.
38
+ `CLAUDE.md` is not an ADR index — `docs/adr/` is its own index, and an ADR earns
39
+ a mention in the instruction files only when an agent needs to know about it.
40
+
41
+ Meanwhile `plugins/librarian/` being absent from the plugin map *was* a real
42
+ defect, and `skills/list-prompt-rules/` is one right now: it appears zero times
43
+ in both `CLAUDE.md` and `AGENTS.md`.
44
+
45
+ So the phase cannot ask "is this mentioned?" in the abstract. It has to be told
46
+ **where completeness is expected**. That is the job of the `globs` config: an
47
+ opt-in list naming the entity classes whose enumeration is supposed to be
48
+ complete. Anything not matched by a glob is never considered, which is what
49
+ keeps `docs/adr/` permanently out of scope without a special case.
50
+
51
+ ## Detection
52
+
53
+ Pure bash. No model call.
54
+
55
+ ```
56
+ for each glob in config.globs:
57
+ for each match under repo_root:
58
+ name = basename(match)
59
+ if not mentioned(name) in DISCOVERED_FILES:
60
+ → finding
61
+ ```
62
+
63
+ Three details carry weight:
64
+
65
+ **Corpus is `DISCOVERED_FILES` only, never `GLOBAL_FILES`.** The user's global
66
+ `~/.claude/CLAUDE.md` has no business documenting one project's plugins. Counting
67
+ a global mention as coverage would let an unrelated personal note silence a real
68
+ project-level omission.
69
+
70
+ **`mentioned()` is a word-boundary match**, `grep -qE "\b<escaped>\b"`, not
71
+ `grep -F`. A substring test would let the word "counseling" satisfy an entity
72
+ named `counsel`. The name is regex-escaped before interpolation.
73
+
74
+ **Detection needs no LLM, and deliberately does not use one.** Presence or
75
+ absence of a name is a grep. A model call would only be useful for judging
76
+ whether an omission *matters*, which is a sharper question than the drift that
77
+ motivated this — `librarian` was absent entirely, and the crudest possible check
78
+ catches that. Adding a materiality judge now would spend a model call per audit
79
+ to solve a problem we have not yet observed. If the false-positive rate turns out
80
+ to warrant it, the `stale_ref` classify pre-pass is the shape to copy.
81
+
82
+ ## Where it sits in the pipeline
83
+
84
+ A third analysis inside `run_synthesize`, alongside `stale_ref` and
85
+ `scope_collision` — **not** a sixth pipeline phase.
86
+
87
+ The phase list (`discover` / `extract` / `relate` / `synthesize` / `emit`) stays
88
+ as it is, so run records keep their current shape and the `phases_completed` and
89
+ `phases_failed` arrays in `runs/audit-<id>.json` do not gain a new member.
90
+ Consumers reading those records do not need to change.
91
+
92
+ New library: `plugins/cartographer/scripts/lib/cartographer-omission.sh`,
93
+ exporting:
94
+
95
+ ```
96
+ cartographer_analyze_undocumented_entity <files_json> <repo_root> <globs_json> \
97
+ <exclude_json> <max_findings>
98
+ ```
99
+
100
+ It prints a JSON array of findings on stdout, matching the shape the other
101
+ analyzers return, so `run_synthesize` merges it into `raw_all` with no special
102
+ handling and the existing hash-enrichment loop covers it unchanged.
103
+
104
+ It is invoked under `$_TIMEOUT_CMD "$_phase_timeout"` like its siblings. The work
105
+ is fast, but a pathological glob over a huge tree should be killed on the same
106
+ terms as everything else rather than being trusted because it is "just bash".
107
+
108
+ ## Finding shape
109
+
110
+ ```json
111
+ {
112
+ "type": "undocumented_entity",
113
+ "severity": "warning",
114
+ "file_a": "<absolute path to the entity>",
115
+ "excerpt_a": "<entity name>",
116
+ "file_b": null,
117
+ "excerpt_b": null,
118
+ "description": "<name> exists at <relative path> but is not mentioned in any instruction file.",
119
+ "suggested_fix": "Document <name> in CLAUDE.md, or exclude its path from cartographer.undocumented_entity."
120
+ }
121
+ ```
122
+
123
+ **`file_a` is the entity, not a document.** There is no single instruction file
124
+ at fault — the entity is missing from the whole corpus, so naming one file would
125
+ be arbitrary. Keying identity on the entity also makes
126
+ `cartographer_finding_hash` stable when someone reorganizes `CLAUDE.md`: a
127
+ finding must not re-fire as new because a heading moved. With `file_b` and
128
+ `excerpt_b` empty, the existing commutative hash degenerates to a stable
129
+ per-entity key, which is exactly the dedup behavior wanted — one finding per
130
+ undocumented entity, once, until it is resolved.
131
+
132
+ **Severity is always `warning`.** The existing definition reserves `error` for
133
+ rules whose violation would compromise safety or produce incorrect output. A
134
+ missing mention is neither.
135
+
136
+ ## Configuration
137
+
138
+ Under the `.cartographer` namespace in `config.json`, overridable through the
139
+ standard five-layer settings overlay (ecosystem `docs/adr/004`):
140
+
141
+ ```json
142
+ "undocumented_entity": {
143
+ "enabled": true,
144
+ "globs": ["plugins/*/", "skills/*/"],
145
+ "exclude": [],
146
+ "max_findings": 20
147
+ }
148
+ ```
149
+
150
+ - **`globs`** — repo-root-relative. The defaults are Claude Code layout
151
+ conventions rather than ecosystem-specific paths, and a glob that matches
152
+ nothing yields no candidates, so this is silently inert in a repository
153
+ without those directories. That is what makes shipping it enabled defensible:
154
+ it is useful out of the box where the convention holds and invisible where it
155
+ does not.
156
+ - **`exclude`** — substring filter over matched paths, mirroring the semantics
157
+ of the existing top-level `exclude_paths`. That field replaces rather than
158
+ merges when a settings layer overrides it (cartographer
159
+ `docs/adr/004-exclude-paths-replace-semantics.md`); this field follows the
160
+ same rule, and the README must say so, because replace-not-merge is the
161
+ behavior users get wrong.
162
+ - **`max_findings`** — see below.
163
+ - **`enabled: false`** short-circuits to `[]` before any filesystem walk.
164
+
165
+ ## Two edge cases that decide correctness
166
+
167
+ **Targeted post-write audits skip this phase entirely.** When
168
+ `CARTOGRAPHER_TARGET_FILE` is set, `run_discover` sets `DISCOVERED_FILES` to that
169
+ one file. Grepping a single file for every entity name in the repository would
170
+ report almost the entire enumeration as undocumented — a burst of false findings
171
+ that would then be dedup-sentineled and never re-evaluated, poisoning the
172
+ findings store permanently. The phase therefore returns `[]` when `TARGET_FILE`
173
+ is non-empty.
174
+
175
+ This follows established precedent rather than inventing one: `scope_collision`
176
+ already no-ops on targeted runs, because `run_discover` sets `GLOBAL_FILES` to
177
+ `[]` and the analyzer returns early on an empty corpus.
178
+
179
+ **First-run noise is capped at `max_findings`.** Enabling this against a
180
+ repository with a thin `CLAUDE.md` could otherwise produce dozens of findings in
181
+ one audit. The count of dropped candidates is written to `audit.log`; a silent
182
+ truncation would read as "this is everything" when it is not.
183
+
184
+ ## Event emission is knowingly broken, and not fixed here
185
+
186
+ `cartographer.issue.found` does not validate against the published schema and
187
+ never has. `run-audit.sh` emits `finding_type` / `affected_files` /
188
+ `finding_hash`; `@onlooker-community/schema` 2.12.0 requires `issue_type` and
189
+ `file_path` with `additionalProperties: false`. `cartographer.audit.complete` is
190
+ off-contract too. Verified by piping both payloads through the real
191
+ `scripts/lib/onlooker-event.mjs emit` path — each exits 1.
192
+
193
+ Per ADR-005 the consequence differs by install mode. In a dev or CI checkout the
194
+ schema package resolves, validation runs, `cartographer_emit_event` returns 1,
195
+ and `emit_safe`'s `|| true` swallows it, so nothing reaches the bus. In an
196
+ installed marketplace plugin there is no `node_modules`, the emitter fails open,
197
+ and off-contract payloads are emitted. Findings still reach
198
+ `findings/<hash>.json` in both modes, which is why `/cartographer` looks healthy
199
+ and this has gone unnoticed.
200
+
201
+ **This is tracked separately as `ecosystem-q4d` (P1) and is out of scope here.**
202
+ Findings from this phase land on disk exactly like the other four, and its bus
203
+ event is broken in exactly the same way until `q4d` lands. Folding the fix in
204
+ would mix a bug fix with a feature and pull an upstream schema change into the
205
+ middle of a feature branch — the same reasoning that split `4d3` / `cs8` / `973`.
206
+
207
+ Worth recording: the published schema's `issue_type` enum already contains
208
+ `orphaned_plugin`, and `issue_categories` already contains `orphaned_plugins`.
209
+ The contract anticipated disk → doc detection; the implementation never built
210
+ it. Whoever resolves `q4d` should decide whether `undocumented_entity` maps onto
211
+ that existing name or whether the enum grows a new member.
212
+
213
+ ## Testing
214
+
215
+ bats, following `test/helpers/setup.bash` and the repo's `writing-tests` skill —
216
+ isolated temp home, no writes to the real `~/.onlooker/`.
217
+
218
+ | Case | Expectation |
219
+ |------|-------------|
220
+ | Entity on disk, name absent from corpus | one finding, `type=undocumented_entity` |
221
+ | Entity on disk, name present in corpus | no finding |
222
+ | Entity `counsel`, corpus says "counseling" | one finding — word boundary holds |
223
+ | Path matches `exclude` | no finding |
224
+ | `enabled: false` | `[]`, no filesystem walk |
225
+ | Glob matching nothing | `[]` |
226
+ | `TARGET_FILE` set | `[]` |
227
+ | Candidates exceed `max_findings` | capped, drop count in `audit.log` |
228
+ | Same entity across two runs | identical `finding_hash` |
229
+
230
+ ## Acceptance
231
+
232
+ Run a full audit against this repository. The phase reports exactly one finding
233
+ — `skills/list-prompt-rules` — and nothing else. All 16 plugins under `plugins/`
234
+ are currently documented in `CLAUDE.md`, and `docs/adr/` is never enumerated.
235
+
236
+ That one-finding result is the acceptance signal in both directions: it proves
237
+ the detection fires on a real gap, and it proves the enumeration is narrow
238
+ enough not to bury that gap in noise.
239
+
240
+ ## Documentation to update
241
+
242
+ - `plugins/cartographer/README.md` — phase list and the new config block.
243
+ - `plugins/cartographer/skills/cartographer/SKILL.md` — the `--phase` value list
244
+ at line 112, and the frontmatter `description`, which enumerates what
245
+ cartographer audits for. The finding renderers at lines 65 and 135 read
246
+ `.type` and `.description` generically and need no change.
247
+ - `CLAUDE.md` — the cartographer row in the plugin map describes the hook
248
+ surface, not the phases, so it likely needs no change. Confirm at
249
+ implementation time.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.34.0",
3
+ "version": "0.43.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",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@biomejs/biome": "2.4.15",
45
- "@onlooker-community/schema": "^2.11.0",
45
+ "@onlooker-community/schema": "^2.15.0",
46
46
  "globals": "^17.6.0",
47
47
  "markdownlint-cli": "^0.48.0"
48
48
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assayer",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Claim verification. At session end, parses the agent's final message for testable claims (\"I ran the tests, they pass\", \"the build is green\") and checks each against the actual command results in the session transcript, classifying it corroborated, contradicted, or unverifiable. Catches lying-without-malice. Advisory by default. Builds on the Onlooker ecosystem plugin.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.3](https://github.com/onlooker-community/ecosystem/compare/assayer-v1.1.2...assayer-v1.1.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
  ## [1.1.2](https://github.com/onlooker-community/ecosystem/compare/assayer-v1.1.1...assayer-v1.1.2) (2026-08-02)
4
11
 
5
12
 
@@ -57,3 +57,9 @@ assayer_config_timeout() {
57
57
  v=$(assayer_config_get '.assayer.evaluation.timeout_seconds')
58
58
  printf '%s' "${v:-60}"
59
59
  }
60
+
61
+ assayer_config_final_message_chars() {
62
+ local v
63
+ v=$(assayer_config_get '.assayer.final_message_chars')
64
+ printf '%s' "${v:-6000}"
65
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cartographer",
3
- "version": "0.3.1",
3
+ "version": "0.6.0",
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,60 @@
2
2
 
3
3
  All notable changes to the Cartographer plugin are documented here.
4
4
 
5
+ ## [0.6.0](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.5.1...cartographer-v0.6.0) (2026-08-19)
6
+
7
+
8
+ ### Features
9
+
10
+ * **cartographer:** announce a finding when its drift is gone :wave: ([#186](https://github.com/onlooker-community/ecosystem/issues/186)) ([f2b57f6](https://github.com/onlooker-community/ecosystem/commit/f2b57f67b3e09f7ec624bce0bb4a306c4e20df8b))
11
+
12
+ ## [0.5.1](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.5.0...cartographer-v0.5.1) (2026-08-19)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **cartographer:** refuse a typeless finding out loud :loudspeaker: ([#182](https://github.com/onlooker-community/ecosystem/issues/182)) ([35ee0af](https://github.com/onlooker-community/ecosystem/commit/35ee0af055fd360a70a77d6ef036bbeb91dcc945))
18
+
19
+ ## [0.5.0](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.4.3...cartographer-v0.5.0) (2026-08-18)
20
+
21
+
22
+ ### Features
23
+
24
+ * **cartographer:** narrow an audit by finding type or subtree :mag: ([#177](https://github.com/onlooker-community/ecosystem/issues/177)) ([94764df](https://github.com/onlooker-community/ecosystem/commit/94764dfc8386521a06c8234bc506cb51a667588d))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **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))
30
+
31
+ ## [0.4.3](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.4.2...cartographer-v0.4.3) (2026-08-17)
32
+
33
+
34
+ ### Bug Fixes
35
+
36
+ * **cartographer:** make the audit read the settings you gave it :gear: ([#174](https://github.com/onlooker-community/ecosystem/issues/174)) ([dc57731](https://github.com/onlooker-community/ecosystem/commit/dc5773165fe561f77e87101ed1fbe6e13bb34c34))
37
+
38
+ ## [0.4.2](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.4.1...cartographer-v0.4.2) (2026-08-17)
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * **cartographer:** let a finding you fixed finally go away :wastebasket: ([#172](https://github.com/onlooker-community/ecosystem/issues/172)) ([c8fdcf4](https://github.com/onlooker-community/ecosystem/commit/c8fdcf40b711b68ee5d90e2c4b6408ea38926900))
44
+
45
+ ## [0.4.1](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.4.0...cartographer-v0.4.1) (2026-08-17)
46
+
47
+
48
+ ### Bug Fixes
49
+
50
+ * **cartographer:** put its events on the bus for the first time :mega: ([#170](https://github.com/onlooker-community/ecosystem/issues/170)) ([91be8d2](https://github.com/onlooker-community/ecosystem/commit/91be8d2c9f171006db12842f3e582d001f9d4e27))
51
+
52
+ ## [0.4.0](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.3.1...cartographer-v0.4.0) (2026-08-16)
53
+
54
+
55
+ ### Features
56
+
57
+ * **cartographer:** detect what no instruction file mentions :eyes: ([#168](https://github.com/onlooker-community/ecosystem/issues/168)) ([31efb7b](https://github.com/onlooker-community/ecosystem/commit/31efb7b035665d5a49a37c1e1af21f519850f7bd))
58
+
5
59
  ## [0.3.1](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.3.0...cartographer-v0.3.1) (2026-08-01)
6
60
 
7
61