@onlooker-community/ecosystem 0.33.0 → 0.34.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 (57) hide show
  1. package/.agents/skills/beads/SKILL.md +80 -0
  2. package/.agents/skills/beads/agents/openai.yaml +4 -0
  3. package/.claude/settings.json +13 -0
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex/config.toml +2 -0
  6. package/.codex/hooks.json +51 -0
  7. package/.markdownlint.json +3 -0
  8. package/.release-please-manifest.json +8 -8
  9. package/AGENTS.md +135 -0
  10. package/CHANGELOG.md +14 -0
  11. package/CLAUDE.md +56 -1
  12. package/docs/lesson-promotion-pipeline.md +210 -0
  13. package/docs/superpowers/plans/2026-08-09-lesson-transform.md +1537 -0
  14. package/docs/superpowers/specs/2026-08-09-lesson-transform-design.md +261 -0
  15. package/package.json +3 -2
  16. package/plugins/archivist/scripts/lib/archivist-config.sh +10 -34
  17. package/plugins/assayer/.claude-plugin/plugin.json +1 -1
  18. package/plugins/assayer/CHANGELOG.md +7 -0
  19. package/plugins/assayer/scripts/lib/assayer-config.sh +39 -62
  20. package/plugins/cartographer/scripts/lib/cartographer-config.sh +11 -29
  21. package/plugins/compass/scripts/lib/compass-config.sh +16 -46
  22. package/plugins/counsel/scripts/lib/counsel-config.sh +15 -46
  23. package/plugins/curator/.claude-plugin/plugin.json +1 -1
  24. package/plugins/curator/CHANGELOG.md +7 -0
  25. package/plugins/curator/scripts/lib/curator-config.sh +19 -44
  26. package/plugins/echo/scripts/lib/echo-config.sh +30 -59
  27. package/plugins/governor/scripts/lib/governor-config.sh +11 -41
  28. package/plugins/historian/scripts/lib/historian-config.sh +9 -33
  29. package/plugins/inspector/.claude-plugin/plugin.json +1 -1
  30. package/plugins/inspector/CHANGELOG.md +7 -0
  31. package/plugins/inspector/scripts/lib/inspector-config.sh +39 -63
  32. package/plugins/librarian/.claude-plugin/plugin.json +1 -1
  33. package/plugins/librarian/CHANGELOG.md +7 -0
  34. package/plugins/librarian/config.json +4 -0
  35. package/plugins/librarian/schema/PROVENANCE.json +7 -0
  36. package/plugins/librarian/schema/lesson-applies-to.subschema.json +74 -0
  37. package/plugins/librarian/schema/lesson-evidence.subschema.json +36 -0
  38. package/plugins/librarian/scripts/hooks/librarian-session-end.sh +26 -0
  39. package/plugins/librarian/scripts/lib/librarian-config.sh +10 -34
  40. package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +135 -0
  41. package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +311 -0
  42. package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +140 -0
  43. package/plugins/lineage/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lineage/CHANGELOG.md +7 -0
  45. package/plugins/lineage/scripts/lib/lineage-config.sh +17 -53
  46. package/plugins/scribe/.claude-plugin/plugin.json +1 -1
  47. package/plugins/scribe/CHANGELOG.md +7 -0
  48. package/plugins/scribe/scripts/lib/scribe-config.sh +17 -47
  49. package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
  50. package/plugins/tribunal/CHANGELOG.md +7 -0
  51. package/plugins/tribunal/scripts/lib/tribunal-config.sh +25 -63
  52. package/plugins/warden/scripts/lib/warden-config.sh +17 -54
  53. package/scripts/lib/config-loader.sh +8 -1
  54. package/scripts/lint/check-lesson-schema-drift.mjs +36 -0
  55. package/test/bats/librarian-lesson-transform.bats +609 -0
  56. package/test/node/lesson-schema-drift.test.mjs +28 -0
  57. package/test/node/lesson-validate-agreement.test.mjs +154 -0
@@ -0,0 +1,261 @@
1
+ # Lesson Transform — Design
2
+
3
+ **Status:** Approved, not started.
4
+ **Tracked by:** `ecosystem-4z8.1`, under epic `ecosystem-4z8`.
5
+ **Parent design:** `docs/superpowers/specs/2026-08-08-promotion-pipeline-design.md` in the
6
+ [onlooker](https://github.com/onlooker-community/onlooker) repo, Sections 2–4.
7
+ That document governs the pipeline. This one covers stage five only, and where
8
+ the two disagree, the disagreements are called out explicitly below.
9
+
10
+ ---
11
+
12
+ ## What this stage is
13
+
14
+ Librarian's fifth stage. It reads one durable, classified, deduped archivist
15
+ artifact and emits a **lesson candidate** — the subset of a Lesson that can be
16
+ inferred from an artifact.
17
+
18
+ It is not a Lesson. `lesson.schema.json` v2 requires thirteen fields; this stage
19
+ owns four:
20
+
21
+ | Field | Owner |
22
+ |---|---|
23
+ | `claim`, `rationale`, `evidence`, `applies_to` | **this stage** |
24
+ | `visibility` | the human, at confirmation (`4z8.2`) |
25
+ | `consensus` | the jury (`4z8.3`) |
26
+ | `id`, `schema_version`, `source`, `status`, `superseded_by`, `author_key`, `promoted_at` | the pool write (`4z8.4`) |
27
+
28
+ Keeping that split explicit is what stops a stage inventing a field it does not
29
+ own. It also answers the open question the epic carried: the transform *cannot*
30
+ validate against `lesson.schema.json`, because the object is deliberately
31
+ incomplete at this point.
32
+
33
+ ## Decisions
34
+
35
+ Four decisions were settled during design. The first and third depart from the
36
+ parent spec; both departures are argued below rather than assumed.
37
+
38
+ **1. No recoverable resolution means declined.** `evidence.resolution` is
39
+ required with `minLength: 1`, and the archivist artifact shape has no resolution
40
+ field — it must be inferred from prose. When it is not there, the artifact is
41
+ declined. This preserves the contract's stance that "this breaks" without "and
42
+ this fixed it" is a warning, not a lesson.
43
+
44
+ **2. Validation is against vendored sub-schemas**, not the full lesson schema,
45
+ and with no network access at runtime. See *Validation* below.
46
+
47
+ **3. The transform emits `versioned` only.** It can never emit
48
+ `version_independent`. If it cannot infer versions, the artifact is declined.
49
+
50
+ The parent spec allows either branch and relies on the jury's `scope_accuracy`
51
+ criterion to catch a lazy justification — "the schema stops the accident, the
52
+ jury stops the lazy excuse." That defense has a hole: **private lessons run no
53
+ jury at all.** A weakly-justified `version_independent` candidate marked private
54
+ reaches the pool with nothing checking it, and a lesson with no version bound
55
+ never expires.
56
+
57
+ That is precisely the motivating failure. The stale vitest artifact was a
58
+ private, local memory that misled its own author across sessions. Closing the
59
+ branch at the transform makes the guarantee structural at every visibility tier
60
+ instead of relying on a gate the private tier skips. `version_independent`
61
+ becomes an explicit human choice at `4z8.2`, made by someone with the context to
62
+ write a justification that is actually true.
63
+
64
+ **4. A cheap pre-gate runs before the model.** Mirrors librarian's existing
65
+ pre-LLM durability filter.
66
+
67
+ ## Architecture
68
+
69
+ New file: `plugins/librarian/scripts/lib/librarian-lesson-transform.sh`, sourced
70
+ by the existing `librarian-session-end.sh` chain and running after conflict/dup
71
+ detection. No new hook. No new plugin — librarian already owns the watermark,
72
+ and a second copy of that state would be free to drift.
73
+
74
+ ### Storage
75
+
76
+ ```
77
+ ~/.onlooker/librarian/<project-key>/lessons/
78
+ proposals/<ulid>.json this stage
79
+ approved/<ulid>.json 4z8.4
80
+ declined.jsonl 4z8.4 (but see Boundary changes)
81
+ ```
82
+
83
+ Lesson state lives in its own subtree rather than reusing librarian's existing
84
+ `proposals/` directory. The two carry different consequences: a memory promotion
85
+ writes to your machine, a lesson proposal is a step toward publishing beyond it.
86
+ `4z8.2`'s open question is specifically the risk of users confusing the two, and
87
+ separate trees mean a confirmation surface cannot merge them by accident.
88
+
89
+ Each candidate carries its source `artifact_id` at the top level, which is
90
+ load-bearing for idempotency.
91
+
92
+ Use `$ONLOOKER_DIR`, never a hardcoded `~/.onlooker`, so the test suite's
93
+ isolated temp home is respected.
94
+
95
+ ## The three steps
96
+
97
+ ### 1. Pre-gate (bash, free)
98
+
99
+ `librarian_lesson_pregate <artifact_json>` scans `summary` and `detail` for a
100
+ version-shaped token (`\d+\.\d+`, `v5`, `5.x`).
101
+
102
+ Because `versioned` is the only branch this stage can emit, an artifact with no
103
+ version token anywhere cannot produce a valid `scope.versions`. Rejecting it is
104
+ a fact about the output shape, not a quality heuristic. The pre-gate rejects
105
+ nothing else — "does this have a resolution" is semantic and stays with the
106
+ model, because a keyword regex would silently drop good candidates.
107
+
108
+ Pre-gate rejections are silent: no event, no ledger entry. `declined.jsonl`
109
+ exists to make expensive drops auditable and to keep re-runs cheap. Re-running a
110
+ regex is free, so recording it would dilute the signal `4z8.4` wants for rubric
111
+ tuning.
112
+
113
+ ### 2. Model call
114
+
115
+ Mirrors `librarian-classifier.sh`: `claude -p --max-turns 1`, model, temperature
116
+ and max-tokens from config, a hard wall-clock timeout so SessionEnd cannot hang,
117
+ fail-soft to empty string, markdown fences stripped before parsing. The timeout
118
+ follows the classifier's ceiling of 20 seconds per call, and like the classifier
119
+ it falls back through `timeout`, `gtimeout`, then no timeout when neither exists.
120
+
121
+ The prompt supplies the artifact in a delimited block and requires one of two
122
+ responses: a candidate object, or an explicit refusal carrying a reason
123
+ (`no_resolution` or `no_versions`). A refusal is a real answer and must be
124
+ distinguishable from a failed call.
125
+
126
+ Config lives in the plugin's `config.json` under
127
+ `librarian.lesson_transform.{model, timeout_seconds}`, read via
128
+ `librarian_config_get` from the caller, with user overrides under the plugin
129
+ namespace key per ADR-004. There is no `enabled` flag — that option was removed
130
+ repo-wide in #108.
131
+
132
+ Two keys only. `claude --help` exposes `--model` and no sampling flags, so
133
+ `temperature` and `max_output_tokens` cannot reach the model through this CLI.
134
+ `librarian-classifier.sh` currently reads both and passes them to a function
135
+ that ignores them — dead config that reads as though it works. Sampling control
136
+ would require moving off `claude -p` to the API, which is a separate decision.
137
+
138
+ ### 3. Validation
139
+
140
+ Two layers, because **ajv is not available at runtime**. It resolves in dev and
141
+ CI only; installed marketplace plugins ship no `node_modules` (ADR-005).
142
+
143
+ - **Runtime:** dependency-free `jq`, which works everywhere.
144
+ - **Source of truth:** sub-schemas vendored at
145
+ `plugins/librarian/schema/lesson-evidence.subschema.json` and
146
+ `lesson-applies-to.subschema.json`, extracted from the published
147
+ `lesson.schema.json`.
148
+ - **Tests:** assert the `jq` validator and the vendored sub-schema agree over a
149
+ corpus. The parent spec sets this precedent directly — the two mechanisms were
150
+ "proven able to disagree," so they are asserted separately.
151
+ - **CI:** a drift guard asserts the vendored copies carry recorded provenance
152
+ (source path and `schema_version: 2`) and have not been edited by hand.
153
+
154
+ Note that this is weaker than the guard used for *event* schemas, which
155
+ compares against `schema.onlooker.dev`. That endpoint currently serves no
156
+ lesson schema — `lesson.schema.json`, `/`, and `lesson/v2.json` all return
157
+ 404 — so a fetch-and-compare guard is not available yet. When lesson schemas
158
+ are published, the guard should be upgraded to fetch-and-compare; until then
159
+ a cross-repo drift can only be caught by a human. Tracked separately.
160
+
161
+ The version-range pattern is where this earns its keep. The schema accepts `<6`,
162
+ `<=6`, `=6`, `>4`, `>=4`, and two-sided `>=4 <6`. It **rejects** npm-style
163
+ `^5.4.21`, `~5`, `5.x`, and bare `5.4.21` — which are exactly what a model
164
+ reaches for by default.
165
+
166
+ It also rejects `>=0`, `>=0.0`, and `>=0.0.0` by construction. An unbounded
167
+ lower bound is version-independence wearing a costume: it matches every session
168
+ and never expires. That closes the same hole decision 3 closes, one layer down.
169
+
170
+ Plus the cross-field rule JSON Schema cannot express: every key of
171
+ `scope.versions` must name an entry in `applies_to.stack`.
172
+
173
+ ## Failure taxonomy
174
+
175
+ Two categories look identical from the call site — both produce no candidate —
176
+ and conflating them is the sharpest risk in this stage.
177
+
178
+ | Condition | Outcome |
179
+ |---|---|
180
+ | pre-gate: no version token | silent skip, nothing written |
181
+ | model returns unparseable JSON | declined, `transform_invalid` |
182
+ | model refuses: no resolution | declined, `no_resolution` |
183
+ | model refuses: cannot infer versions | declined, `no_versions` |
184
+ | candidate fails sub-schema validation | declined, `schema_invalid` |
185
+ | `versions` key not named in `stack` | declined, `schema_invalid` |
186
+ | `claude` CLI absent | **untouched**, no ledger entry |
187
+ | timeout, or empty response | **untouched**, no ledger entry |
188
+
189
+ A verdict says "this artifact cannot become a lesson." An outage says "we could
190
+ not tell." Recording an outage as a verdict buries a good artifact permanently,
191
+ because the watermark has moved past it and declined entries are never re-read.
192
+
193
+ ## Idempotency
194
+
195
+ **The watermark cannot provide it.** `last_scan.json` is `{scanned_at: <ISO>}` —
196
+ a wall-clock timestamp stamped at scan time, not an artifact-id set. The parent
197
+ spec's failure table states that the watermark, ledger and pool are "all keyed
198
+ by `artifact_id`"; that is true of the ledger and pool but not the watermark.
199
+
200
+ Before spending a model call, skip when the `artifact_id` already appears in
201
+ `lessons/proposals/`, `lessons/declined.jsonl`, or `lessons/approved/`. That
202
+ check is what actually delivers "the same artifact cannot be promoted twice" and
203
+ "re-runs are cheap."
204
+
205
+ Librarian's existing tombstones do not help here: they are keyed by body hash
206
+ with a 180-day TTL, built for "the user rejected this memory, stop offering it."
207
+ Lesson idempotency must be artifact-keyed and permanent.
208
+
209
+ ## Testing
210
+
211
+ bats, using the repo's isolated-temp-home helpers.
212
+
213
+ **The parent spec's acceptance test inverts.** It designates artifact
214
+ `01KZ45MKAM734ZS7JK24D2DK0R` — the real vitest/vite mismatch — and expects it to
215
+ transform to `kind: versioned` with `{vite: "<6", vitest: ">=4"}`. Under decision
216
+ 1 it cannot: the artifact records only that the mismatch is real, and its session
217
+ ended with an open question titled "What is the correct fix for the vitest/vite
218
+ version mismatch?" No resolution was ever found. Neither does its later
219
+ refutation artifact (`01KZEAF9EY4C6TTR0V7YFN9VYJ`), which says the claim was
220
+ disproven without stating what is true.
221
+
222
+ So it becomes the **negative** test: declined, `no_resolution`. The artifact that
223
+ motivated the whole design proves the resolution rule fires.
224
+
225
+ The positive test uses a synthetic fixture, since no artifact on disk carries
226
+ both a resolution and versions. Shape it like the vitest case so it still
227
+ asserts `{vite: "<6", vitest: ">=4"}`.
228
+
229
+ Also:
230
+
231
+ - validator agreement over a range corpus — `^5.4.21`, `~5`, `5.x`, `5.4.21`,
232
+ `>=0`, `>=0.0.0` must all fail; `<6`, `>=4`, `>=4 <6`, `=6` must pass
233
+ - infrastructure is not a verdict: with `claude` absent, nothing is written to
234
+ `declined.jsonl` or anywhere else. This is the test most likely to regress
235
+ silently
236
+ - idempotency: an `artifact_id` already in `declined.jsonl` triggers no model call
237
+ - cross-field: a `versions` key absent from `stack` is declined
238
+ - pre-gate: an artifact with no version token produces no ledger entry
239
+
240
+ ## Events
241
+
242
+ `librarian.lesson.proposed` and `librarian.lesson.declined`, emitted through
243
+ `scripts/lib/onlooker-event.mjs`. Both must be registered in
244
+ `@onlooker-community/schema` before they are emitted.
245
+
246
+ ## Boundary changes to sibling issues
247
+
248
+ Building this stage first has two consequences for the issues around it:
249
+
250
+ - **`4z8.4`** currently owns `declined.jsonl` and its append function. The
251
+ transform produces declines, so the storage primitive moves here and `4z8.4`
252
+ reuses it.
253
+ - **`4z8.2`** gains the `version_independent` path from decision 3, including
254
+ where a human writes and edits the justification.
255
+
256
+ ## Out of scope
257
+
258
+ Version inference from anything but artifact prose. Reading the project's
259
+ current `package.json` or lockfile would report today's versions, not the
260
+ versions in effect when the problem was observed — and the gap between those two
261
+ is the entire staleness problem this pipeline exists to solve.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.33.0",
3
+ "version": "0.34.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",
@@ -25,10 +25,11 @@
25
25
  "test:shellcheck": "shellcheck -S error -x install.sh scripts/common.sh scripts/hooks/*.sh scripts/lib/*.sh plugins/archivist/scripts/hooks/*.sh plugins/archivist/scripts/lib/*.sh plugins/tribunal/scripts/hooks/*.sh plugins/tribunal/scripts/lib/*.sh plugins/echo/scripts/hooks/*.sh plugins/echo/scripts/lib/*.sh plugins/governor/scripts/hooks/*.sh plugins/governor/scripts/lib/*.sh plugins/compass/scripts/hooks/*.sh plugins/compass/scripts/lib/*.sh plugins/scribe/scripts/hooks/*.sh plugins/scribe/scripts/lib/*.sh plugins/counsel/scripts/hooks/*.sh plugins/counsel/scripts/lib/*.sh plugins/warden/scripts/hooks/*.sh plugins/warden/scripts/lib/*.sh plugins/librarian/scripts/hooks/*.sh plugins/librarian/scripts/lib/*.sh plugins/curator/scripts/hooks/*.sh plugins/curator/scripts/lib/*.sh plugins/historian/scripts/hooks/*.sh plugins/historian/scripts/lib/*.sh plugins/assayer/scripts/hooks/*.sh plugins/assayer/scripts/lib/*.sh plugins/cartographer/scripts/hooks/*.sh plugins/cartographer/scripts/lib/*.sh plugins/bursar/scripts/hooks/*.sh plugins/bursar/scripts/lib/*.sh plugins/lineage/scripts/hooks/*.sh plugins/lineage/scripts/lib/*.sh plugins/inspector/scripts/hooks/*.sh plugins/inspector/scripts/lib/*.sh",
26
26
  "lint:references": "node scripts/lint/check-references.mjs",
27
27
  "lint:manifests": "node scripts/lint/check-manifests.mjs",
28
+ "lint:lesson-schema": "node scripts/lint/check-lesson-schema-drift.mjs",
28
29
  "coverage:node": "node scripts/coverage/run-coverage.mjs",
29
30
  "coverage:bash": "node scripts/coverage/bash-coverage.mjs",
30
31
  "coverage": "npm run coverage:node && npm run coverage:bash",
31
- "test:ci": "npm run test:shellcheck && npm run test:bats && npm run test:schema && npm run lint:check && npm run lint:manifests && npm run lint:references",
32
+ "test:ci": "npm run test:shellcheck && npm run test:bats && npm run test:schema && npm run lint:check && npm run lint:manifests && npm run lint:references && npm run lint:lesson-schema",
32
33
  "lint:check": "biome check . && markdownlint '**/*.md'",
33
34
  "lint": "biome lint --write && markdownlint --fix '**/*.md'",
34
35
  "format": "biome format --write && markdownlint --fix '**/*.md'",
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env bash
2
2
  # Config resolution for Archivist.
3
3
  #
4
- # Reads three layers, latest wins:
4
+ # Uses the shared config loader from ecosystem. Reads five layers, latest wins:
5
5
  # 1. plugins/archivist/config.json (defaults shipped with the plugin)
6
6
  # 2. ~/.claude/settings.json
7
- # 3. <repo>/.claude/settings.json
7
+ # 3. ~/.claude/settings.local.json (local overrides user)
8
+ # 4. <repo>/.claude/settings.json
9
+ # 5. <repo>/.claude/settings.local.json (local overrides project)
8
10
  #
9
11
  # Exposes:
10
12
  # archivist_config_load <repo_root> # populates _ARCHIVIST_CONFIG (JSON)
@@ -13,46 +15,20 @@
13
15
  # Settings overlay only touches the `archivist.*` subtree of settings.json so it
14
16
  # coexists with other plugins' configuration.
15
17
 
18
+ # shellcheck source=../../../scripts/lib/config-loader.sh
19
+ source "${PLUGIN_ROOT}/../../scripts/lib/config-loader.sh"
20
+
16
21
  _ARCHIVIST_CONFIG="{}"
17
22
 
18
23
  archivist_config_load() {
19
24
  local repo_root="${1:-}"
20
- local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
21
- local home_dir="${HOME:-}"
22
-
23
- local merged="{}"
24
- local file
25
-
26
- file="${plugin_root}/config.json"
27
- if [[ -f "$file" ]]; then
28
- local defaults
29
- defaults=$(jq '.' "$file" 2>/dev/null) || defaults="{}"
30
- merged=$(jq -n --argjson a "$merged" --argjson b "$defaults" '$a * $b' 2>/dev/null) \
31
- || merged="$defaults"
32
- fi
33
-
34
- for file in "${home_dir}/.claude/settings.json" "${repo_root}/.claude/settings.json"; do
35
- [[ -n "$file" && -f "$file" ]] || continue
36
- local overlay
37
- overlay=$(jq '{ archivist: (.archivist // {}) }' "$file" 2>/dev/null) || continue
38
- [[ -z "$overlay" ]] && continue
39
- merged=$(jq -n --argjson a "$merged" --argjson b "$overlay" '
40
- def deepmerge($a; $b):
41
- if ($a|type) == "object" and ($b|type) == "object" then
42
- reduce (($a|keys) + ($b|keys) | unique)[] as $k
43
- ({}; .[$k] = deepmerge($a[$k]; $b[$k]))
44
- elif $b == null then $a
45
- else $b end;
46
- deepmerge($a; $b)
47
- ' 2>/dev/null) || true
48
- done
49
-
50
- _ARCHIVIST_CONFIG="$merged"
25
+ config_load_plugin "archivist" "$repo_root" "_ARCHIVIST_CONFIG"
26
+ return 0
51
27
  }
52
28
 
53
29
  # Read a value from the loaded config. Usage:
54
30
  # archivist_config_get '.archivist.injection.max_items'
55
31
  archivist_config_get() {
56
32
  local path="$1"
57
- printf '%s' "$_ARCHIVIST_CONFIG" | jq -r "${path} // empty" 2>/dev/null
33
+ config_get "_ARCHIVIST_CONFIG" "${path}"
58
34
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assayer",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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.2](https://github.com/onlooker-community/ecosystem/compare/assayer-v1.1.1...assayer-v1.1.2) (2026-08-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * restore config convenience functions & refactor execution plugins to shared loader ([#128](https://github.com/onlooker-community/ecosystem/issues/128)) ([4b3660c](https://github.com/onlooker-community/ecosystem/commit/4b3660c5a8b234187ec1e71c37b63e6a3d305c98))
9
+
3
10
  ## [1.1.1](https://github.com/onlooker-community/ecosystem/compare/assayer-v1.1.0...assayer-v1.1.1) (2026-08-01)
4
11
 
5
12
 
@@ -1,82 +1,59 @@
1
1
  #!/usr/bin/env bash
2
- # Config loading for Assayer.
3
- # Reads the repo's .claude/settings.json assayer.* keys, falling back to the
4
- # plugin's own config.json defaults.
5
-
6
- _ASSAYER_CONFIG_JSON=""
7
- _ASSAYER_PLUGIN_CONFIG_JSON=""
2
+ # Config resolution for assayer.
3
+ #
4
+ # Uses the shared config loader from ecosystem. Reads five layers, latest wins:
5
+ # 1. plugins/assayer/config.json (defaults shipped with the plugin)
6
+ # 2. ~/.claude/settings.json
7
+ # 3. ~/.claude/settings.local.json (local overrides user)
8
+ # 4. <repo>/.claude/settings.json
9
+ # 5. <repo>/.claude/settings.local.json (local overrides project)
10
+ #
11
+ # Exposes:
12
+ # assayer_config_load <repo_root> # populates _assayer_CONFIG (JSON)
13
+ # assayer_config_get <jq-path> # echoes string value (empty if unset)
14
+ # assayer_config_get_json <jq-path> # echoes JSON value (null if unset)
15
+
16
+ # shellcheck source=../../../scripts/lib/config-loader.sh
17
+ source "${PLUGIN_ROOT}/../../scripts/lib/config-loader.sh"
18
+
19
+ _assayer_CONFIG="{}"
8
20
 
9
21
  assayer_config_load() {
10
22
  local repo_root="${1:-}"
11
-
12
- _ASSAYER_PLUGIN_CONFIG_JSON=""
13
- local plugin_config="${CLAUDE_PLUGIN_ROOT:-}/config.json"
14
- if [[ -f "$plugin_config" ]]; then
15
- _ASSAYER_PLUGIN_CONFIG_JSON=$(cat "$plugin_config" 2>/dev/null) || _ASSAYER_PLUGIN_CONFIG_JSON=""
16
- fi
17
-
18
- _ASSAYER_CONFIG_JSON=""
19
- if [[ -n "$repo_root" ]]; then
20
- local settings_file="${repo_root}/.claude/settings.json"
21
- if [[ -f "$settings_file" ]]; then
22
- local settings
23
- settings=$(cat "$settings_file" 2>/dev/null) || settings=""
24
- local block
25
- block=$(printf '%s' "$settings" | jq -c '.assayer // empty' 2>/dev/null) || block=""
26
- [[ -n "$block" ]] && _ASSAYER_CONFIG_JSON="$block"
27
- fi
28
- fi
23
+ config_load_plugin "assayer" "$repo_root" "_assayer_CONFIG"
24
+ return 0
29
25
  }
30
26
 
31
- # Get a single scalar value. Checks settings.json first, then plugin config.json.
32
27
  assayer_config_get() {
33
- local key="$1"
34
-
35
- if [[ -n "$_ASSAYER_CONFIG_JSON" ]]; then
36
- local val
37
- val=$(printf '%s' "$_ASSAYER_CONFIG_JSON" | jq -r "${key} // empty" 2>/dev/null) || val=""
38
- [[ -n "$val" && "$val" != "null" ]] && {
39
- printf '%s' "$val"
40
- return 0
41
- }
42
- fi
43
-
44
- if [[ -n "$_ASSAYER_PLUGIN_CONFIG_JSON" ]]; then
45
- local val
46
- val=$(printf '%s' "$_ASSAYER_PLUGIN_CONFIG_JSON" | jq -r ".assayer${key} // empty" 2>/dev/null) || val=""
47
- [[ -n "$val" && "$val" != "null" ]] && {
48
- printf '%s' "$val"
49
- return 0
50
- }
51
- fi
28
+ local path="$1"
29
+ config_get "_assayer_CONFIG" "${path}"
52
30
  }
53
31
 
54
- assayer_config_model() {
55
- local val
56
- val=$(assayer_config_get '.evaluation.model')
57
- printf '%s' "${val:-claude-haiku-4-5-20251001}"
32
+ assayer_config_get_json() {
33
+ local path="$1"
34
+ config_get_json "_assayer_CONFIG" "${path}"
58
35
  }
59
36
 
60
- assayer_config_timeout() {
61
- local val
62
- val=$(assayer_config_get '.evaluation.timeout_seconds')
63
- printf '%s' "${val:-60}"
37
+ assayer_config_model() {
38
+ local v
39
+ v=$(assayer_config_get '.assayer.evaluation.model')
40
+ printf '%s' "${v:-claude-haiku-4-5-20251001}"
64
41
  }
65
42
 
66
43
  assayer_config_max_claims() {
67
- local val
68
- val=$(assayer_config_get '.max_claims')
69
- printf '%s' "${val:-12}"
44
+ local v
45
+ v=$(assayer_config_get '.assayer.max_claims')
46
+ printf '%s' "${v:-12}"
70
47
  }
71
48
 
72
49
  assayer_config_min_confidence() {
73
- local val
74
- val=$(assayer_config_get '.min_confidence')
75
- printf '%s' "${val:-0.5}"
50
+ local v
51
+ v=$(assayer_config_get '.assayer.min_confidence')
52
+ printf '%s' "${v:-0.5}"
76
53
  }
77
54
 
78
- assayer_config_final_message_chars() {
79
- local val
80
- val=$(assayer_config_get '.final_message_chars')
81
- printf '%s' "${val:-6000}"
55
+ assayer_config_timeout() {
56
+ local v
57
+ v=$(assayer_config_get '.assayer.evaluation.timeout_seconds')
58
+ printf '%s' "${v:-60}"
82
59
  }
@@ -1,54 +1,36 @@
1
1
  #!/usr/bin/env bash
2
2
  # cartographer-config.sh — load and query Cartographer configuration.
3
3
  #
4
- # Merges three layers in precedence order (later wins):
4
+ # Uses the shared config loader from ecosystem. Merges five layers in precedence order:
5
5
  # 1. plugins/cartographer/config.json (plugin defaults)
6
6
  # 2. ~/.claude/settings.json (.cartographer subtree)
7
- # 3. <repo>/.claude/settings.json (.cartographer subtree)
7
+ # 3. ~/.claude/settings.local.json (.cartographer subtree, local overrides user)
8
+ # 4. <repo>/.claude/settings.json (.cartographer subtree)
9
+ # 5. <repo>/.claude/settings.local.json (.cartographer subtree, local overrides project)
8
10
  #
9
11
  # Usage:
10
12
  # cartographer_config_load <repo_root>
11
13
  # cartographer_config_get_json ".cartographer.exclude_paths"
12
14
 
15
+ # shellcheck source=../../../scripts/lib/config-loader.sh
16
+ source "${PLUGIN_ROOT}/../../scripts/lib/config-loader.sh"
17
+
13
18
  _CARTOGRAPHER_CONFIG=""
14
- _CARTOGRAPHER_PLUGIN_CONFIG=""
15
19
 
16
20
  cartographer_config_load() {
17
21
  local repo_root="${1:-}"
18
- local plugin_dir
19
- plugin_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
20
- local plugin_config="$plugin_dir/config.json"
21
-
22
- _CARTOGRAPHER_PLUGIN_CONFIG="{}"
23
- if [[ -f "$plugin_config" ]]; then
24
- _CARTOGRAPHER_PLUGIN_CONFIG=$(cat "$plugin_config")
25
- fi
26
-
27
- local home_settings="{}"
28
- if [[ -f "$HOME/.claude/settings.json" ]]; then
29
- home_settings=$(cat "$HOME/.claude/settings.json")
30
- fi
31
-
32
- local repo_settings="{}"
33
- if [[ -n "$repo_root" && -f "$repo_root/.claude/settings.json" ]]; then
34
- repo_settings=$(cat "$repo_root/.claude/settings.json")
35
- fi
36
-
37
- _CARTOGRAPHER_CONFIG=$(jq -n \
38
- --argjson plugin "$_CARTOGRAPHER_PLUGIN_CONFIG" \
39
- --argjson home "$home_settings" \
40
- --argjson repo "$repo_settings" \
41
- '$plugin * {"cartographer": (($plugin.cartographer // {}) * ($home.cartographer // {}) * ($repo.cartographer // {}))}')
22
+ config_load_plugin "cartographer" "$repo_root" "_CARTOGRAPHER_CONFIG"
23
+ return 0
42
24
  }
43
25
 
44
26
  cartographer_config_get() {
45
27
  local path="${1:-}"
46
- printf '%s' "$_CARTOGRAPHER_CONFIG" | jq -r "$path // empty" 2>/dev/null
28
+ config_get "_CARTOGRAPHER_CONFIG" "${path}"
47
29
  }
48
30
 
49
31
  cartographer_config_get_json() {
50
32
  local path="${1:-}"
51
- printf '%s' "$_CARTOGRAPHER_CONFIG" | jq -c "$path // empty" 2>/dev/null
33
+ config_get_json "_CARTOGRAPHER_CONFIG" "${path}"
52
34
  }
53
35
 
54
36
  cartographer_config_model_extraction() {
@@ -1,65 +1,35 @@
1
1
  #!/usr/bin/env bash
2
- # Config resolution for Compass.
2
+ # Config resolution for compass.
3
3
  #
4
- # Reads three layers, latest wins:
4
+ # Uses the shared config loader from ecosystem. Reads five layers, latest wins:
5
5
  # 1. plugins/compass/config.json (defaults shipped with the plugin)
6
6
  # 2. ~/.claude/settings.json
7
- # 3. <repo>/.claude/settings.json
7
+ # 3. ~/.claude/settings.local.json (local overrides user)
8
+ # 4. <repo>/.claude/settings.json
9
+ # 5. <repo>/.claude/settings.local.json (local overrides project)
8
10
  #
9
11
  # Exposes:
10
- # compass_config_load <repo_root> # populates _COMPASS_CONFIG (JSON)
11
- # compass_config_get <jq-path> # echoes string value (empty if unset)
12
- # compass_config_get_json <jq-path> # echoes JSON value (null if unset)
12
+ # compass_config_load <repo_root> # populates _compass_CONFIG (JSON)
13
+ # compass_config_get <jq-path> # echoes string value (empty if unset)
14
+ # compass_config_get_json <jq-path> # echoes JSON value (null if unset)
13
15
 
14
- _COMPASS_CONFIG="{}"
16
+ # shellcheck source=../../../scripts/lib/config-loader.sh
17
+ source "${PLUGIN_ROOT}/../../scripts/lib/config-loader.sh"
18
+
19
+ _compass_CONFIG="{}"
15
20
 
16
21
  compass_config_load() {
17
22
  local repo_root="${1:-}"
18
- local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
19
- local home_dir="${HOME:-}"
20
-
21
- local merged="{}"
22
- local file
23
-
24
- file="${plugin_root}/config.json"
25
- if [[ -f "$file" ]]; then
26
- local defaults
27
- defaults=$(jq '.' "$file" 2>/dev/null) || defaults="{}"
28
- merged=$(jq -n --argjson a "$merged" --argjson b "$defaults" '$a * $b' 2>/dev/null) \
29
- || merged="$defaults"
30
- fi
31
-
32
- local repo_settings=""
33
- [[ -n "$repo_root" ]] && repo_settings="${repo_root}/.claude/settings.json"
34
-
35
- for file in "${home_dir}/.claude/settings.json" "$repo_settings"; do
36
- [[ -n "$file" && -f "$file" ]] || continue
37
- local overlay
38
- overlay=$(jq '{ compass: (.compass // {}) }' "$file" 2>/dev/null) || continue
39
- [[ -z "$overlay" ]] && continue
40
- local attempt
41
- if attempt=$(jq -n --argjson a "$merged" --argjson b "$overlay" '
42
- def deepmerge($a; $b):
43
- if ($a|type) == "object" and ($b|type) == "object" then
44
- reduce (($a|keys) + ($b|keys) | unique)[] as $k
45
- ({}; .[$k] = deepmerge($a[$k]; $b[$k]))
46
- elif $b == null then $a
47
- else $b end;
48
- deepmerge($a; $b)
49
- ' 2>/dev/null) && [[ -n "$attempt" ]]; then
50
- merged="$attempt"
51
- fi
52
- done
53
-
54
- _COMPASS_CONFIG="$merged"
23
+ config_load_plugin "compass" "$repo_root" "_compass_CONFIG"
24
+ return 0
55
25
  }
56
26
 
57
27
  compass_config_get() {
58
28
  local path="$1"
59
- printf '%s' "$_COMPASS_CONFIG" | jq -r "${path} // empty" 2>/dev/null
29
+ config_get "_compass_CONFIG" "${path}"
60
30
  }
61
31
 
62
32
  compass_config_get_json() {
63
33
  local path="$1"
64
- printf '%s' "$_COMPASS_CONFIG" | jq -c "${path}" 2>/dev/null
34
+ config_get_json "_compass_CONFIG" "${path}"
65
35
  }