@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,741 @@
1
+ # Cartographer `undocumented_entity` Phase Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Give cartographer a disk → doc analysis phase so an entity that exists on disk and is mentioned in no instruction file becomes visible.
6
+
7
+ **Architecture:** A new pure-bash analyzer library, `cartographer-omission.sh`, enumerates configured globs under the repo root and word-boundary-greps each entity's name across the instruction corpus. It is invoked as a third analysis inside `run_synthesize`, alongside `stale_ref` and `scope_collision`, and returns findings in the same shape so the existing hash-enrichment and emit loops handle it unchanged. No model call.
8
+
9
+ **Tech Stack:** bash (targeting 3.2 — macOS system bash), `jq`, `grep -E`, bats for tests.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md`
12
+ **Tracked by:** `ecosystem-3eu`
13
+
14
+ ## Global Constraints
15
+
16
+ - All hook and library code is bash. No Python, no Node entry points. Shelling out to `node` for event emission is allowed.
17
+ - Event types follow `<plugin>.<noun>.<verb>`. **This plan emits no new event type** — findings ride the existing `cartographer.issue.found`.
18
+ - Runtime artifacts go under `${ONLOOKER_DIR:-$HOME/.onlooker}/`. Never hardcode `~/.onlooker`.
19
+ - American English in all comments, identifiers, and docs.
20
+ - Tests target bash 3.2 semantics: any **non-final** `[[ ]]` assertion in a bats body must be gated with `|| return 1`, or it does not fail the test on macOS.
21
+ - Config defaults live in `plugins/cartographer/config.json` under the `cartographer` namespace key.
22
+ - `npm run test:ci` must pass — it adds shellcheck and the manifest/reference linters on top of the tests.
23
+
24
+ ## Out of scope, deliberately
25
+
26
+ Two pre-existing bugs sit adjacent to this work. **Do not fix either one in this branch.**
27
+
28
+ - **`ecosystem-q4d` (P1)** — both cartographer event payloads are off-contract with the published schema, so `cartographer.issue.found` never validates. Findings from this phase land on disk correctly and their bus event is broken in exactly the same way as the existing four phases. That is expected. Do not touch the emit path.
29
+ - **`ecosystem-88v` (P2)** — `run-audit.sh` never calls `cartographer_config_load` at the top level, and does not export `PLUGIN_ROOT`, so the `cartographer_config_load` calls inside the three analysis sub-shells fail too. **Task 3 works around this** by setting `PLUGIN_ROOT` inside its own sub-shell. That inline assignment is intentional, is commented as such, and should be removed once `88v` lands.
30
+
31
+ ## File Structure
32
+
33
+ | File | Responsibility |
34
+ |------|----------------|
35
+ | `plugins/cartographer/scripts/lib/cartographer-omission.sh` | **Create.** The analyzer. Enumeration, mention test, finding construction, cap. Nothing else. |
36
+ | `plugins/cartographer/scripts/lib/cartographer-config.sh` | **Modify.** Four accessors for the new config block. |
37
+ | `plugins/cartographer/config.json` | **Modify.** Shipped defaults. |
38
+ | `plugins/cartographer/scripts/run-audit.sh` | **Modify.** Wire the analyzer into `run_synthesize`; guard on targeted audits. |
39
+ | `test/bats/cartographer-omission.bats` | **Create.** Analyzer unit tests. |
40
+ | `test/bats/cartographer-config.bats` | **Modify.** Config accessor tests. |
41
+ | `plugins/cartographer/README.md` | **Modify.** Phase list and config block. |
42
+ | `plugins/cartographer/skills/cartographer/SKILL.md` | **Modify.** `--phase` values and frontmatter description. |
43
+
44
+ The analyzer lives in its own file rather than being appended to `cartographer-analyze.sh` because that file is exclusively LLM-calling analyzers sharing one prompt-and-parse idiom. This one calls no model and shares none of it.
45
+
46
+ ---
47
+
48
+ ### Task 1: Config surface
49
+
50
+ **Files:**
51
+
52
+ - Modify: `plugins/cartographer/config.json`
53
+ - Modify: `plugins/cartographer/scripts/lib/cartographer-config.sh:66-80`
54
+ - Test: `test/bats/cartographer-config.bats`
55
+
56
+ **Interfaces:**
57
+
58
+ - Consumes: `cartographer_config_get`, `cartographer_config_get_json` (existing, same file).
59
+ - Produces: `cartographer_config_undocumented_enabled` → `"true"`/`"false"`; `cartographer_config_undocumented_globs` → JSON array; `cartographer_config_undocumented_exclude` → JSON array; `cartographer_config_undocumented_max_findings` → integer. Task 3 calls all four.
60
+
61
+ - [ ] **Step 1: Write the failing tests**
62
+
63
+ Append to `test/bats/cartographer-config.bats`:
64
+
65
+ ```bash
66
+ @test "undocumented_entity: defaults ship enabled with plugin and skill globs" {
67
+ cartographer_config_load ""
68
+ local enabled globs max
69
+ enabled=$(cartographer_config_undocumented_enabled)
70
+ globs=$(cartographer_config_undocumented_globs)
71
+ max=$(cartographer_config_undocumented_max_findings)
72
+ [ "$enabled" = "true" ]
73
+ [ "$max" = "20" ]
74
+ [ "$(printf '%s' "$globs" | jq -r 'length')" = "2" ]
75
+ [ "$(printf '%s' "$globs" | jq -r '.[0]')" = "plugins/*/" ]
76
+ }
77
+
78
+ @test "undocumented_entity: user settings can disable the phase" {
79
+ mkdir -p "${HOME}/.claude"
80
+ printf '%s\n' '{"cartographer":{"undocumented_entity":{"enabled":false}}}' \
81
+ > "${HOME}/.claude/settings.json"
82
+ cartographer_config_load ""
83
+ local enabled max
84
+ enabled=$(cartographer_config_undocumented_enabled)
85
+ max=$(cartographer_config_undocumented_max_findings)
86
+ [ "$enabled" = "false" ]
87
+ [ "$max" = "20" ]
88
+ }
89
+
90
+ @test "undocumented_entity: repo settings replace the glob list wholesale" {
91
+ local repo="${BATS_TEST_TMPDIR}/repo-glob"
92
+ mkdir -p "${repo}/.claude"
93
+ printf '%s\n' '{"cartographer":{"undocumented_entity":{"globs":["agents/*/"]}}}' \
94
+ > "${repo}/.claude/settings.json"
95
+ cartographer_config_load "$repo"
96
+ local globs
97
+ globs=$(cartographer_config_undocumented_globs)
98
+ [ "$(printf '%s' "$globs" | jq -r 'length')" = "1" ]
99
+ [ "$(printf '%s' "$globs" | jq -r '.[0]')" = "agents/*/" ]
100
+ }
101
+
102
+ @test "undocumented_entity: exclude defaults to empty" {
103
+ cartographer_config_load ""
104
+ local excl
105
+ excl=$(cartographer_config_undocumented_exclude)
106
+ [ "$(printf '%s' "$excl" | jq -r 'length')" = "0" ]
107
+ }
108
+ ```
109
+
110
+ - [ ] **Step 2: Run tests to verify they fail**
111
+
112
+ Run: `bats test/bats/cartographer-config.bats`
113
+ Expected: FAIL — `cartographer_config_undocumented_enabled: command not found`.
114
+
115
+ - [ ] **Step 3: Add the config defaults**
116
+
117
+ In `plugins/cartographer/config.json`, add a sibling to `exclude_paths` inside the `cartographer` object (mind the comma on the `exclude_paths` line):
118
+
119
+ ```json
120
+ "exclude_paths": ["node_modules", ".git", "vendor", ".venv", "dist", ".next", ".nuxt", "build", "__pycache__"],
121
+ "undocumented_entity": {
122
+ "enabled": true,
123
+ "globs": ["plugins/*/", "skills/*/"],
124
+ "exclude": [],
125
+ "max_findings": 20
126
+ }
127
+ ```
128
+
129
+ - [ ] **Step 4: Add the accessors**
130
+
131
+ Append to `plugins/cartographer/scripts/lib/cartographer-config.sh`:
132
+
133
+ ```bash
134
+ # ── undocumented_entity phase ──────────────────────────────────────────────────
135
+ # Disk → doc detection. Unlike the other phases these are read inside the
136
+ # analysis sub-shell rather than by the orchestrator; see run-audit.sh and
137
+ # ecosystem-88v for why.
138
+
139
+ cartographer_config_undocumented_enabled() {
140
+ local v
141
+ v=$(cartographer_config_get '.cartographer.undocumented_entity.enabled')
142
+ printf '%s' "${v:-true}"
143
+ }
144
+
145
+ cartographer_config_undocumented_globs() {
146
+ cartographer_config_get_json \
147
+ '.cartographer.undocumented_entity.globs // ["plugins/*/","skills/*/"]'
148
+ }
149
+
150
+ cartographer_config_undocumented_exclude() {
151
+ cartographer_config_get_json '.cartographer.undocumented_entity.exclude // []'
152
+ }
153
+
154
+ cartographer_config_undocumented_max_findings() {
155
+ local v
156
+ v=$(cartographer_config_get '.cartographer.undocumented_entity.max_findings')
157
+ printf '%s' "${v:-20}"
158
+ }
159
+ ```
160
+
161
+ - [ ] **Step 5: Run tests to verify they pass**
162
+
163
+ Run: `bats test/bats/cartographer-config.bats`
164
+ Expected: PASS, all tests including the four pre-existing ones.
165
+
166
+ - [ ] **Step 6: Break one assertion on purpose**
167
+
168
+ Temporarily change `[ "$enabled" = "false" ]` to `[ "$enabled" = "true" ]` in the disable test, re-run, and confirm it **fails**. Revert. A test that passes either way reports coverage that does not exist.
169
+
170
+ - [ ] **Step 7: Commit**
171
+
172
+ ```bash
173
+ git add plugins/cartographer/config.json \
174
+ plugins/cartographer/scripts/lib/cartographer-config.sh \
175
+ test/bats/cartographer-config.bats
176
+ ```
177
+
178
+ Then run `/commit` — do not hand-write `git commit -m`. Suggested subject:
179
+ `feat(cartographer): add config for the disk-to-doc phase :gear:`
180
+
181
+ ---
182
+
183
+ ### Task 2: Detection library
184
+
185
+ **Files:**
186
+
187
+ - Create: `plugins/cartographer/scripts/lib/cartographer-omission.sh`
188
+ - Test: `test/bats/cartographer-omission.bats` (create)
189
+
190
+ **Interfaces:**
191
+
192
+ - Consumes: nothing from Task 1. This library is standalone and takes every input as a parameter, matching how the analyzers in `cartographer-analyze.sh` are written.
193
+ - Produces: `cartographer_analyze_undocumented_entity <files_json> <repo_root> <globs_json> <exclude_json> <max_findings>` → prints a JSON array on stdout. Task 3 calls it. Also `_cartographer_name_mentioned <name> <files_json>` → exit 0 if mentioned; private, but bats tests it directly.
194
+
195
+ - [ ] **Step 1: Write the failing tests**
196
+
197
+ Create `test/bats/cartographer-omission.bats`:
198
+
199
+ ```bash
200
+ #!/usr/bin/env bats
201
+
202
+ setup() {
203
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
204
+ setup_test_env
205
+
206
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/cartographer"
207
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
208
+ # shellcheck disable=SC1091
209
+ source "${PLUGIN_ROOT}/scripts/lib/cartographer-omission.sh"
210
+
211
+ FIXTURE_REPO="${BATS_TEST_TMPDIR}/repo"
212
+ mkdir -p "${FIXTURE_REPO}/plugins/alpha" \
213
+ "${FIXTURE_REPO}/plugins/beta" \
214
+ "${FIXTURE_REPO}/skills/solo"
215
+ DOC="${FIXTURE_REPO}/CLAUDE.md"
216
+ printf '# Doc\nThe alpha plugin does things.\n' > "$DOC"
217
+ CORPUS=$(jq -n --arg f "$DOC" '[$f]')
218
+ }
219
+
220
+ @test "flags an entity whose name appears nowhere in the corpus" {
221
+ local out
222
+ out=$(cartographer_analyze_undocumented_entity \
223
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
224
+ [[ "$(printf '%s' "$out" | jq -r 'length')" == "1" ]] || return 1
225
+ [[ "$(printf '%s' "$out" | jq -r '.[0].excerpt_a')" == "beta" ]] || return 1
226
+ [ "$(printf '%s' "$out" | jq -r '.[0].type')" = "undocumented_entity" ]
227
+ }
228
+
229
+ @test "does not flag an entity the corpus mentions" {
230
+ local out
231
+ out=$(cartographer_analyze_undocumented_entity \
232
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
233
+ [ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "alpha")] | length')" = "0" ]
234
+ }
235
+
236
+ @test "finding carries the entity as file_a and a null file_b" {
237
+ local out
238
+ out=$(cartographer_analyze_undocumented_entity \
239
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
240
+ [[ "$(printf '%s' "$out" | jq -r '.[0].file_a')" == "${FIXTURE_REPO}/plugins/beta" ]] || return 1
241
+ [[ "$(printf '%s' "$out" | jq -r '.[0].file_b')" == "null" ]] || return 1
242
+ [ "$(printf '%s' "$out" | jq -r '.[0].severity')" = "warning" ]
243
+ }
244
+
245
+ @test "word boundary: a longer word containing the name does not count as a mention" {
246
+ printf '# Doc\nWe do a lot of counseling here.\n' > "$DOC"
247
+ mkdir -p "${FIXTURE_REPO}/plugins/counsel"
248
+ local out
249
+ out=$(cartographer_analyze_undocumented_entity \
250
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
251
+ [ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "counsel")] | length')" = "1" ]
252
+ }
253
+
254
+ @test "word boundary: a hyphenated name is not matched inside a longer hyphenated token" {
255
+ printf '# Doc\nSee my-list-prompt-rules-thing for details.\n' > "$DOC"
256
+ mkdir -p "${FIXTURE_REPO}/skills/list-prompt-rules"
257
+ local out
258
+ out=$(cartographer_analyze_undocumented_entity \
259
+ "$CORPUS" "$FIXTURE_REPO" '["skills/*/"]' '[]' 20)
260
+ [ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "list-prompt-rules")] | length')" = "1" ]
261
+ }
262
+
263
+ @test "word boundary: a name bounded by slashes counts as a mention" {
264
+ printf '# Doc\nSee plugins/beta/ for details.\n' > "$DOC"
265
+ local out
266
+ out=$(cartographer_analyze_undocumented_entity \
267
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
268
+ [ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "beta")] | length')" = "0" ]
269
+ }
270
+
271
+ @test "exclude filters a matched path by substring" {
272
+ local out
273
+ out=$(cartographer_analyze_undocumented_entity \
274
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/","skills/*/"]' '["skills/"]' 20)
275
+ [[ "$(printf '%s' "$out" | jq -r 'length')" == "1" ]] || return 1
276
+ [ "$(printf '%s' "$out" | jq -r '.[0].excerpt_a')" = "beta" ]
277
+ }
278
+
279
+ @test "max_findings caps the result and reports the drop count on stderr" {
280
+ local out err
281
+ err="${BATS_TEST_TMPDIR}/err.txt"
282
+ out=$(cartographer_analyze_undocumented_entity \
283
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/","skills/*/"]' '[]' 1 2>"$err")
284
+ [[ "$(printf '%s' "$out" | jq -r 'length')" == "1" ]] || return 1
285
+ grep -q "1 candidate" "$err"
286
+ }
287
+
288
+ @test "a glob matching nothing yields an empty array" {
289
+ local out
290
+ out=$(cartographer_analyze_undocumented_entity \
291
+ "$CORPUS" "$FIXTURE_REPO" '["nonexistent/*/"]' '[]' 20)
292
+ [ "$out" = "[]" ]
293
+ }
294
+
295
+ @test "an empty corpus yields an empty array rather than flagging everything" {
296
+ local out
297
+ out=$(cartographer_analyze_undocumented_entity \
298
+ '[]' "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
299
+ [ "$out" = "[]" ]
300
+ }
301
+
302
+ @test "the same entity produces an identical finding hash across two runs" {
303
+ # shellcheck disable=SC1091
304
+ source "${PLUGIN_ROOT}/scripts/lib/cartographer-analyze.sh"
305
+ local a b h1 h2
306
+ a=$(cartographer_analyze_undocumented_entity \
307
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
308
+ b=$(cartographer_analyze_undocumented_entity \
309
+ "$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
310
+ h1=$(cartographer_finding_hash "undocumented_entity" \
311
+ "$(printf '%s' "$a" | jq -r '.[0].file_a')" \
312
+ "$(printf '%s' "$a" | jq -r '.[0].excerpt_a')" "" "")
313
+ h2=$(cartographer_finding_hash "undocumented_entity" \
314
+ "$(printf '%s' "$b" | jq -r '.[0].file_a')" \
315
+ "$(printf '%s' "$b" | jq -r '.[0].excerpt_a')" "" "")
316
+ [[ -n "$h1" ]] || return 1
317
+ [ "$h1" = "$h2" ]
318
+ }
319
+ ```
320
+
321
+ - [ ] **Step 2: Run tests to verify they fail**
322
+
323
+ Run: `bats test/bats/cartographer-omission.bats`
324
+ Expected: FAIL — the source in `setup()` errors, no such file.
325
+
326
+ - [ ] **Step 3: Write the implementation**
327
+
328
+ Create `plugins/cartographer/scripts/lib/cartographer-omission.sh`:
329
+
330
+ ```bash
331
+ #!/usr/bin/env bash
332
+ # cartographer-omission.sh — disk → doc detection.
333
+ #
334
+ # Every other analysis phase starts from the text of the instruction files and
335
+ # tests what it finds against the filesystem. This one runs the other way: it
336
+ # enumerates entities on disk and checks each is mentioned somewhere in the
337
+ # corpus. An entity nothing names produces no token for stale_ref to extract
338
+ # and no rule for contradiction to compare, which is why omissions were
339
+ # previously invisible to every phase.
340
+ #
341
+ # Detection is a grep — no model call. A model would only help judge whether an
342
+ # omission MATTERS, which is a sharper question than the drift that motivated
343
+ # this. See docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md
344
+ #
345
+ # Usage:
346
+ # cartographer_analyze_undocumented_entity <files_json> <repo_root> \
347
+ # <globs_json> <exclude_json> <max_findings>
348
+ #
349
+ # Prints a JSON array of findings on stdout in the same shape the analyzers in
350
+ # cartographer-analyze.sh return, so run_synthesize merges it without special
351
+ # handling. Diagnostics go to stderr, which run-audit.sh appends to audit.log.
352
+
353
+ # Word-boundary mention test.
354
+ #
355
+ # Boundaries are hand-rolled rather than \b because entity names contain
356
+ # hyphens, and \b treats '-' as a non-word character: \blist-prompt-rules\b
357
+ # would also match inside "my-list-prompt-rules-thing". Bounding on
358
+ # [^A-Za-z0-9_-] instead means a hyphenated name matches only when genuinely
359
+ # standalone, while a name inside a path ("plugins/beta/") still counts.
360
+ _cartographer_name_mentioned() {
361
+ local name="$1"
362
+ local files_json="$2"
363
+
364
+ local escaped
365
+ escaped=$(printf '%s' "$name" | sed 's/[][\.*^$(){}?+|\\]/\\&/g')
366
+
367
+ local fpath
368
+ while IFS= read -r fpath; do
369
+ [[ -z "$fpath" || ! -f "$fpath" ]] && continue
370
+ if grep -qE "(^|[^A-Za-z0-9_-])${escaped}([^A-Za-z0-9_-]|\$)" "$fpath" 2>/dev/null; then
371
+ return 0
372
+ fi
373
+ done < <(printf '%s' "$files_json" | jq -r '.[]' 2>/dev/null)
374
+ return 1
375
+ }
376
+
377
+ cartographer_analyze_undocumented_entity() {
378
+ local files_json="${1:-[]}"
379
+ local repo_root="${2:?repo_root required}"
380
+ local globs_json="${3:-[]}"
381
+ local exclude_json="${4:-[]}"
382
+ local max_findings="${5:-20}"
383
+
384
+ # An empty corpus cannot document anything, so every entity would look
385
+ # undocumented. Refuse rather than emit a burst of false findings that the
386
+ # emit phase would dedup-sentinel and never re-evaluate.
387
+ local corpus_count
388
+ corpus_count=$(printf '%s' "$files_json" | jq 'length' 2>/dev/null || printf '0')
389
+ [[ "${corpus_count:-0}" -eq 0 ]] && { printf '[]'; return 0; }
390
+
391
+ local findings="[]"
392
+ local emitted=0 dropped=0
393
+ local root="${repo_root%/}"
394
+
395
+ # nullglob so an unmatched pattern expands to nothing rather than to itself.
396
+ # Restore the prior setting — this library is sourced, not run.
397
+ local had_nullglob=0
398
+ shopt -q nullglob && had_nullglob=1
399
+ shopt -s nullglob
400
+
401
+ local glob match
402
+ while IFS= read -r glob; do
403
+ [[ -z "$glob" ]] && continue
404
+ # shellcheck disable=SC2086 # unquoted on purpose: this is the glob expansion
405
+ for match in ${root}/$glob; do
406
+ [[ -e "$match" ]] || continue
407
+
408
+ local trimmed="${match%/}"
409
+ local name relpath
410
+ name=$(basename "$trimmed")
411
+ relpath="${trimmed#"${root}"/}"
412
+
413
+ local excluded=0 excl
414
+ while IFS= read -r excl; do
415
+ [[ -z "$excl" ]] && continue
416
+ [[ "$relpath" == *"$excl"* ]] && { excluded=1; break; }
417
+ done < <(printf '%s' "$exclude_json" | jq -r '.[]' 2>/dev/null)
418
+ [[ "$excluded" -eq 1 ]] && continue
419
+
420
+ _cartographer_name_mentioned "$name" "$files_json" && continue
421
+
422
+ if [[ "$emitted" -ge "$max_findings" ]]; then
423
+ dropped=$(( dropped + 1 ))
424
+ continue
425
+ fi
426
+
427
+ local finding
428
+ finding=$(jq -n \
429
+ --arg fa "$trimmed" \
430
+ --arg n "$name" \
431
+ --arg rp "$relpath" \
432
+ '{
433
+ type: "undocumented_entity",
434
+ severity: "warning",
435
+ file_a: $fa,
436
+ excerpt_a: $n,
437
+ file_b: null,
438
+ excerpt_b: null,
439
+ description: ($n + " exists at " + $rp
440
+ + " but is not mentioned in any instruction file."),
441
+ suggested_fix: ("Document " + $n
442
+ + " in CLAUDE.md, or exclude its path from cartographer.undocumented_entity.")
443
+ }')
444
+ findings=$(printf '%s' "$findings" | jq --argjson f "$finding" '. + [$f]')
445
+ emitted=$(( emitted + 1 ))
446
+ done
447
+ done < <(printf '%s' "$globs_json" | jq -r '.[]' 2>/dev/null)
448
+
449
+ [[ "$had_nullglob" -eq 0 ]] && shopt -u nullglob
450
+
451
+ # Say what was dropped. A silent truncation reads as "this is everything".
452
+ if [[ "$dropped" -gt 0 ]]; then
453
+ printf 'undocumented_entity: capped at %s findings, %s candidate(s) dropped\n' \
454
+ "$max_findings" "$dropped" >&2
455
+ fi
456
+
457
+ printf '%s' "$findings"
458
+ }
459
+ ```
460
+
461
+ - [ ] **Step 4: Run tests to verify they pass**
462
+
463
+ Run: `bats test/bats/cartographer-omission.bats`
464
+ Expected: PASS, 11 tests.
465
+
466
+ - [ ] **Step 5: Run shellcheck**
467
+
468
+ Run: `npm run test:shellcheck`
469
+ Expected: clean. The only suppression is the deliberate `SC2086` on the glob-expansion line; if shellcheck flags anything else, fix the code rather than adding a suppression.
470
+
471
+ - [ ] **Step 6: Break one assertion on purpose**
472
+
473
+ In the hyphen boundary test, temporarily change the expected `1` to `0`, re-run, confirm it **fails**, revert. That test is the one most likely to silently pass for the wrong reason.
474
+
475
+ - [ ] **Step 7: Commit**
476
+
477
+ ```bash
478
+ git add plugins/cartographer/scripts/lib/cartographer-omission.sh \
479
+ test/bats/cartographer-omission.bats
480
+ ```
481
+
482
+ Then run `/commit`. Suggested subject:
483
+ `feat(cartographer): detect entities no instruction file mentions :mag:`
484
+
485
+ ---
486
+
487
+ ### Task 3: Pipeline integration and documentation
488
+
489
+ **Files:**
490
+
491
+ - Modify: `plugins/cartographer/scripts/run-audit.sh:142-197` (`run_synthesize`) and the header comment at lines 7-12
492
+ - Modify: `plugins/cartographer/README.md`
493
+ - Modify: `plugins/cartographer/skills/cartographer/SKILL.md:3` and `:112`
494
+ - Test: `test/bats/cartographer-omission.bats` (append integration tests)
495
+
496
+ **Interfaces:**
497
+
498
+ - Consumes: `cartographer_analyze_undocumented_entity` (Task 2); `cartographer_config_undocumented_enabled` / `_globs` / `_exclude` / `_max_findings` (Task 1).
499
+ - Produces: nothing new for later tasks. This is the last task.
500
+
501
+ - [ ] **Step 1: Write the failing integration tests**
502
+
503
+ Append to `test/bats/cartographer-omission.bats`:
504
+
505
+ ```bash
506
+ # Stubs `claude` so the three LLM phases make no real call, then runs a full
507
+ # audit. Any extra env the caller needs is exported before calling this.
508
+ _run_audit() {
509
+ local stub="${BATS_TEST_TMPDIR}/bin"
510
+ mkdir -p "$stub"
511
+ printf '#!/usr/bin/env bash\ncat >/dev/null\nprintf "[]"\n' > "${stub}/claude"
512
+ chmod +x "${stub}/claude"
513
+
514
+ PATH="${stub}:${PATH}" \
515
+ CARTOGRAPHER_DIR="${BATS_TEST_TMPDIR}/state" \
516
+ CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" \
517
+ CARTOGRAPHER_TRIGGER="manual" \
518
+ CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" \
519
+ bash "${PLUGIN_ROOT}/scripts/run-audit.sh"
520
+ }
521
+
522
+ # run_emit writes findings with a bare `jq`, which pretty-prints — the file
523
+ # contains `"type": "undocumented_entity"` with a space. Parse rather than grep.
524
+ _findings_of_type() {
525
+ local dir="${BATS_TEST_TMPDIR}/state/findings"
526
+ local count=0 f
527
+ [[ -d "$dir" ]] || { printf '0'; return 0; }
528
+ for f in "$dir"/*.json; do
529
+ [[ -e "$f" ]] || continue
530
+ if [[ "$(jq -r '.type // ""' "$f" 2>/dev/null)" == "undocumented_entity" ]]; then
531
+ count=$(( count + 1 ))
532
+ fi
533
+ done
534
+ printf '%s' "$count"
535
+ }
536
+
537
+ @test "integration: a full audit records the undocumented entity on disk" {
538
+ _run_audit
539
+ [ "$(_findings_of_type)" = "1" ]
540
+ }
541
+
542
+ @test "integration: a targeted post-write audit records no undocumented entity" {
543
+ export CARTOGRAPHER_TARGET_FILE="$DOC"
544
+ _run_audit
545
+ [ "$(_findings_of_type)" = "0" ]
546
+ }
547
+
548
+ @test "integration: enabled=false suppresses the phase" {
549
+ mkdir -p "${FIXTURE_REPO}/.claude"
550
+ printf '%s\n' '{"cartographer":{"undocumented_entity":{"enabled":false}}}' \
551
+ > "${FIXTURE_REPO}/.claude/settings.json"
552
+ _run_audit
553
+ [ "$(_findings_of_type)" = "0" ]
554
+ }
555
+ ```
556
+
557
+ - [ ] **Step 2: Run tests to verify they fail**
558
+
559
+ Run: `bats test/bats/cartographer-omission.bats`
560
+ Expected: the first integration test FAILS with `0 != 1`; the other two pass vacuously (nothing runs the phase yet). Only the first proves anything at this point.
561
+
562
+ - [ ] **Step 3: Source the new library in run-audit.sh**
563
+
564
+ After line 32 (`source "$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh"`), add:
565
+
566
+ ```bash
567
+ source "$PLUGIN_ROOT/scripts/lib/cartographer-omission.sh"
568
+ ```
569
+
570
+ - [ ] **Step 4: Wire the phase into run_synthesize**
571
+
572
+ In `run_synthesize`, immediately after the `scope_findings=$(...)` assignment and before the `# Merge all raw findings` comment, insert:
573
+
574
+ ```bash
575
+ # Disk → doc. Skipped on targeted post-write audits: DISCOVERED_FILES is a
576
+ # single file there, so grepping it for every entity name would report
577
+ # nearly the whole enumeration as undocumented, and the emit phase would
578
+ # dedup-sentinel those false findings permanently. scope_collision already
579
+ # no-ops on targeted runs for the same reason.
580
+ #
581
+ # Config is read inside the sub-shell rather than passed in, because the
582
+ # orchestrator's own config is never loaded (ecosystem-88v). PLUGIN_ROOT is
583
+ # assigned here because run-audit.sh does not export it, which is why the
584
+ # sibling phases' cartographer_config_load calls fail. Remove both
585
+ # workarounds when 88v lands.
586
+ local omission_findings="[]"
587
+ if [[ -z "$TARGET_FILE" ]]; then
588
+ omission_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
589
+ "PLUGIN_ROOT='$PLUGIN_ROOT'
590
+ source '$PLUGIN_ROOT/scripts/lib/cartographer-config.sh'
591
+ source '$PLUGIN_ROOT/scripts/lib/cartographer-omission.sh'
592
+ cartographer_config_load '$REPO_ROOT'
593
+ [[ \"\$(cartographer_config_undocumented_enabled)\" == 'true' ]] \
594
+ || { printf '[]'; exit 0; }
595
+ cartographer_analyze_undocumented_entity '$DISCOVERED_FILES' '$REPO_ROOT' \
596
+ \"\$(cartographer_config_undocumented_globs)\" \
597
+ \"\$(cartographer_config_undocumented_exclude)\" \
598
+ \"\$(cartographer_config_undocumented_max_findings)\"" \
599
+ 2>>"$CARTOGRAPHER_DIR/audit.log") || omission_findings="[]"
600
+ fi
601
+ ```
602
+
603
+ - [ ] **Step 5: Merge the findings**
604
+
605
+ Replace the `raw_all=$(jq -n ...)` block with:
606
+
607
+ ```bash
608
+ local raw_all
609
+ raw_all=$(jq -n \
610
+ --argjson relate "${RELATE_FINDINGS:-[]}" \
611
+ --argjson stale "${stale_findings:-[]}" \
612
+ --argjson scope "${scope_findings:-[]}" \
613
+ --argjson omission "${omission_findings:-[]}" \
614
+ '$relate + $stale + $scope + $omission')
615
+ ```
616
+
617
+ - [ ] **Step 6: Update the file header comment**
618
+
619
+ In `run-audit.sh`, change line 11 from:
620
+
621
+ ```text
622
+ # 4. synthesize — stale_ref + scope_collision + finding hash computation
623
+ ```
624
+
625
+ to:
626
+
627
+ ```text
628
+ # 4. synthesize — stale_ref + scope_collision + undocumented_entity + hash
629
+ ```
630
+
631
+ - [ ] **Step 7: Run tests to verify they pass**
632
+
633
+ Run: `bats test/bats/cartographer-omission.bats`
634
+ Expected: PASS, 14 tests.
635
+
636
+ - [ ] **Step 8: Verify against this repository**
637
+
638
+ Run:
639
+
640
+ ```bash
641
+ CARTOGRAPHER_DIR="$(mktemp -d)" \
642
+ CARTOGRAPHER_REPO_ROOT="$PWD" \
643
+ CARTOGRAPHER_TRIGGER=manual \
644
+ CLAUDE_PLUGIN_ROOT="$PWD/plugins/cartographer" \
645
+ bash plugins/cartographer/scripts/run-audit.sh
646
+ ```
647
+
648
+ Expected: exactly one `undocumented_entity` finding, for `skills/list-prompt-rules`. All 16 plugins are documented and `docs/adr/` is never enumerated. If a second finding appears, the enumeration is too broad — do not proceed until it is one.
649
+
650
+ - [ ] **Step 9: Update the README**
651
+
652
+ In `plugins/cartographer/README.md`, add a row to the "What it detects" table
653
+ after the `scope_collision` row at line 16:
654
+
655
+ ```markdown
656
+ | `undocumented_entity` | Something that exists on disk — a plugin, a skill — that no instruction file mentions |
657
+ ```
658
+
659
+ Then add to the "Configuration" section, after the `exclude_paths` note at
660
+ line 75:
661
+
662
+ ````markdown
663
+ ### Detecting omissions
664
+
665
+ Every other check reads the instruction files and tests what it finds against
666
+ the filesystem. `undocumented_entity` runs the other way: it enumerates
667
+ entities on disk and flags any whose name appears in no instruction file. That
668
+ is the one kind of drift the other checks structurally cannot see — something
669
+ absent produces no reference to follow.
670
+
671
+ ```json
672
+ {
673
+ "cartographer": {
674
+ "undocumented_entity": {
675
+ "enabled": true,
676
+ "globs": ["plugins/*/", "skills/*/"],
677
+ "exclude": [],
678
+ "max_findings": 20
679
+ }
680
+ }
681
+ }
682
+ ```
683
+
684
+ `globs` are relative to the repository root, and a glob matching nothing is
685
+ simply inert — the defaults do nothing in a repository without those
686
+ directories. The list is deliberately opt-in: most of a repository has no
687
+ business being named in `CLAUDE.md`, so only classes where you expect the
688
+ documentation to be *complete* belong here.
689
+
690
+ **Note:** as with `exclude_paths`, overriding `globs` or `exclude` replaces the
691
+ entire list rather than extending it. Repeat the defaults alongside your
692
+ additions if you mean to extend.
693
+ ````
694
+
695
+ - [ ] **Step 10: Update the skill**
696
+
697
+ In `plugins/cartographer/skills/cartographer/SKILL.md`:
698
+
699
+ - Line 112: change the `--phase` list to `contradiction`, `stale_ref`, `dead_rule`, `scope_collision`, or `undocumented_entity`.
700
+ - Line 3 frontmatter `description`: add omissions to what it audits for, e.g. `...dead rules, scope collisions, and entities no instruction file mentions.`
701
+
702
+ The finding renderers at lines 65 and 135 read `.type` and `.description` generically — **do not change them.**
703
+
704
+ - [ ] **Step 11: Confirm CLAUDE.md needs no change**
705
+
706
+ The cartographer row in the plugin map describes the hook surface, not the phases. Read it and confirm. If it does mention phases, update it.
707
+
708
+ - [ ] **Step 12: Run the full CI suite**
709
+
710
+ Run: `npm run test:ci`
711
+ Expected: PASS — shellcheck, bats, schema, biome, markdownlint, manifest and reference linters.
712
+
713
+ - [ ] **Step 13: Commit**
714
+
715
+ ```bash
716
+ git add plugins/cartographer/scripts/run-audit.sh \
717
+ plugins/cartographer/README.md \
718
+ plugins/cartographer/skills/cartographer/SKILL.md \
719
+ test/bats/cartographer-omission.bats
720
+ ```
721
+
722
+ Then run `/commit`. Suggested subject:
723
+ `feat(cartographer): run the omission phase in every full audit :eyes:`
724
+
725
+ - [ ] **Step 14: Close the bead and open the PR**
726
+
727
+ ```bash
728
+ bd close ecosystem-3eu --reason="undocumented_entity phase ships; detection, config, wiring, tests, docs"
729
+ ```
730
+
731
+ Then open a PR with `/git-workflow:pr`. Never push to `main` — release-please needs every change to travel through a PR. Note in the PR body that `ecosystem-q4d` and `ecosystem-88v` were found during this work and deliberately left out.
732
+
733
+ ---
734
+
735
+ ## Notes for the implementer
736
+
737
+ **Why the sub-shell looks awkward.** The three sibling phases pass config in as positional parameters. This one reads config inside its own sub-shell instead. That is not a style preference — the orchestrator never loads config (`ecosystem-88v`), so a value read at the top of `run-audit.sh` would always be the shipped default and the `enabled: false` off-switch would be inert. The `PLUGIN_ROOT='...'` assignment is there for the same reason. Both go away when `88v` lands; the comment in the code says so.
738
+
739
+ **Do not "fix" the emit path.** You will notice while testing that no `cartographer.issue.found` event reaches `$ONLOOKER_EVENTS_LOG`. That is `ecosystem-q4d`, it predates this work, and it affects all five phases equally. Findings on disk are the correct assertion surface for these tests.
740
+
741
+ **bash 3.2.** `mapfile`/`readarray` are unavailable. Do not reach for them. `shopt -s nullglob` and `while read` are the portable idioms, and both are used above.