@onlooker-community/ecosystem 0.43.2 → 0.43.3

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.
@@ -0,0 +1,255 @@
1
+ # Repo-Wide Schema Emission Harness — Design
2
+
3
+ **Status:** Approved, not started.
4
+ **Tracked by:** `ecosystem-u0t`.
5
+ **Single repository.** Everything lands under `scripts/lib/`, `test/`, and
6
+ `package.json`.
7
+
8
+ ---
9
+
10
+ ## What this is
11
+
12
+ Three bugs now share one root cause. A hook built a payload that diverged from
13
+ what the published schema admits; the emitter rejected it; the hook swallowed
14
+ the rejection and exited 0; the on-disk artifacts kept working. Nothing looked
15
+ broken.
16
+
17
+ - `ecosystem-q4d` (P1, closed) — both cartographer payloads off-contract.
18
+ - `ecosystem-1p1` (P2, closed) — librarian `scan.complete` emitted
19
+ `outcome:budget_exceeded`, which the enum rejects.
20
+ - `ecosystem-ci0` (P3, open) — cartographer `finding_type:"unknown"`, admitted
21
+ by no schema.
22
+
23
+ Each fix added its own bats test driving a real payload through
24
+ `scripts/lib/onlooker-event.mjs`. That guarantee stops at the plugin being
25
+ repaired. Nothing generalizes it, so the next divergence ships the same way:
26
+ silently, with green tests and working artifacts. Every instance so far was
27
+ found by hand, long after it landed, and each cost a two-repo release cycle.
28
+
29
+ This builds one harness that makes the class detectable.
30
+
31
+ ## Why the obvious seam is the wrong one
32
+
33
+ `ecosystem-u0t` proposes enumerating payload builders per plugin, noting that
34
+ the `q4d` fix extracted cartographer's builders out of `run_emit` "precisely so
35
+ tests and production share one construction — that pattern is the likely seam,
36
+ but not every plugin has been refactored that way yet."
37
+
38
+ The seam does not exist. Counting named `*_payload()` builders across every
39
+ `plugins/*/scripts/lib/*-events.sh`:
40
+
41
+ | Plugin | Builders |
42
+ |--------|----------|
43
+ | cartographer | 3 |
44
+ | every other events lib (12) | 0 |
45
+
46
+ Three plugins — curator, historian, librarian — have no events lib at all.
47
+ Everywhere except cartographer, the payload is assembled inline at the call
48
+ site and handed to `<plugin>_emit_event` as a finished JSON string.
49
+
50
+ Choosing that seam means refactoring fifteen plugins before writing a single
51
+ assertion, and the refactor would be driven by a test rather than by need.
52
+ Rejected.
53
+
54
+ ## The failure signature
55
+
56
+ The rejection is not a bad line on the bus. It is the **absence** of a line.
57
+
58
+ Probing the emitter directly with a payload that violates
59
+ `governor.gate.checked`:
60
+
61
+ ```
62
+ EMITTER EXIT=1
63
+ stderr: [ { "path": "/decision",
64
+ "message": "must be equal to one of the allowed values" }, ... ]
65
+ bus: NO LOG FILE — event was dropped
66
+ ```
67
+
68
+ Per [ADR-005](../../adr/005-runtime-emitter-fails-open.md), validation is
69
+ attempted through a lazy `await import('@onlooker-community/schema')`. Where
70
+ the package resolves — dev, CI, tests — the emitter validates and **rejects**.
71
+ Where it does not, it emits anyway. So in CI an invalid payload never reaches
72
+ the log, and sweeping the log for invalid lines would find nothing, ever.
73
+
74
+ Hooks fail soft and exit 0 by design, which discards the emitter's exit 1 and
75
+ its stderr. That is the whole bug: the only two signals the emitter produces
76
+ are both destroyed before anyone can see them. `ecosystem-1p1` established the
77
+ usable property in the other direction — presence on the bus proves the payload
78
+ validated.
79
+
80
+ The harness therefore has to capture the rejection at the point it happens, in
81
+ a place that outlives the hook.
82
+
83
+ ## Architecture
84
+
85
+ ```
86
+ 107 bats files -> real hooks -> real branches
87
+ |
88
+ scripts/lib/onlooker-event.mjs
89
+ |
90
+ tryValidate(event)
91
+ +---------------+---------------+
92
+ valid rejected
93
+ | |
94
+ bus (per-test, dropped, exit 1,
95
+ ephemeral) stderr swallowed
96
+ +---------------+---------------+
97
+ |
98
+ $ONLOOKER_TEST_REPORT_DIR/emissions.jsonl <- durable
99
+ { event_type, validated, valid, errors? }
100
+ |
101
+ Gate A Gate B
102
+ no valid:false line manifest accounts for 125
103
+ ```
104
+
105
+ ## Why a durable report directory
106
+
107
+ `test/helpers/setup.bash:15` points `ONLOOKER_DIR` at
108
+ `${BATS_TEST_TMPDIR}/home/.onlooker`. Bats deletes that tree after **each
109
+ test**, so a sidecar written under `$ONLOOKER_DIR` dies with the test that
110
+ produced it and neither gate could read it afterward.
111
+
112
+ A shared bats `teardown` was considered and rejected. Teardown is per-file: a
113
+ definition in the shared helper would clobber the one bats file that already
114
+ defines its own, and any future file defining a teardown would silently opt out
115
+ of the gate — the same silent-opt-out failure mode that produced `u0t`.
116
+
117
+ Instead the suite exports `ONLOOKER_TEST_REPORT_DIR` once, outside
118
+ `BATS_TEST_TMPDIR`. One mechanism then feeds both gates, with no per-test
119
+ opt-in to forget.
120
+
121
+ ## The emitter change
122
+
123
+ In `scripts/lib/onlooker-event.mjs`, after `tryValidate`: when
124
+ `ONLOOKER_TEST_REPORT_DIR` is set, append one JSON line recording the event
125
+ type, whether validation actually ran, whether it passed, and any ajv errors.
126
+
127
+ Constraints:
128
+
129
+ - Guarded entirely on the environment variable. Production never sets it, so
130
+ production behavior, exit codes, and the fail-open contract are untouched.
131
+ - One `appendFileSync` of one line, so the record is atomic under POSIX
132
+ `O_APPEND` for the line sizes involved.
133
+ - Records **`validated`** separately from **`valid`**. This is not redundant;
134
+ see below.
135
+
136
+ Roughly twelve lines. It is the only production file this design touches.
137
+
138
+ ## Gate A — no rejected emission
139
+
140
+ Fails if any line in `emissions.jsonl` has `valid:false`, reporting the event
141
+ type and the ajv errors that caused it.
142
+
143
+ It additionally asserts that **at least one line recorded `validated:true`**.
144
+ Without that assertion the gate is a trap. If `node_modules` is ever absent,
145
+ `tryValidate` fails open, nothing validates, no line is ever `valid:false`, and
146
+ Gate A passes green while checking nothing at all — reproducing the exact bug it
147
+ exists to catch, one level up. Recording whether validation ran is what makes
148
+ that state detectable rather than indistinguishable from success.
149
+
150
+ ## Gate B — every registered type accounted for
151
+
152
+ A committed manifest, `test/bus-coverage.json`:
153
+
154
+ ```json
155
+ {
156
+ "expected": ["session.start", "governor.gate.checked", "..."],
157
+ "excluded": {
158
+ "meridian.hint.generated": "meridian plugin lives in another repo",
159
+ "curator.finding.contradiction": "LLM contradiction sweep deferred",
160
+ "compass.check.overridden": "compass has no implementation yet"
161
+ }
162
+ }
163
+ ```
164
+
165
+ Gate B asserts three things:
166
+
167
+ 1. Every type in `expected` appears in `emissions.jsonl` with `valid:true`.
168
+ 2. `expected` and `excluded` together equal `ALL_EVENT_TYPES` exactly — no
169
+ gaps, no extras.
170
+ 3. Every exclusion carries a non-empty reason.
171
+
172
+ Assertion 2 is the anti-drift property. A newly registered schema type belongs
173
+ to neither list, so CI fails until someone triages it deliberately. This is the
174
+ same shape as the `test:shellcheck` fix in #190: derive coverage from the
175
+ authoritative set rather than from a hand-maintained list that rots.
176
+
177
+ All three assertions are hard from the first commit: the manifest is authored
178
+ during implementation precisely to make them true. What ratchets is the
179
+ manifest itself, as types move from `excluded` to `expected` when the features
180
+ that emit them land.
181
+
182
+ ## Where the gates run
183
+
184
+ The gates become their own npm script, `test:bus`, sequenced after `test:bats`
185
+ in `test:ci`. It **fails when the report is missing**.
186
+
187
+ They deliberately do not live inside `test:schema`. A node test that skips when
188
+ the report is absent would pass vacuously for anyone running `test:schema`
189
+ standalone — silent skipping being, again, the failure mode under repair.
190
+
191
+ `test:bats` clears and exports the report directory; the path is gitignored.
192
+
193
+ ## Bootstrapping the manifest
194
+
195
+ Of 125 registered types, 75 are referenced in this repo's source and 50 are
196
+ not. The 50 split three ways, and each group takes a different disposition:
197
+
198
+ | Group | Count | Disposition |
199
+ |-------|-------|-------------|
200
+ | Foreign plugins — meridian 6, sentinel 3, oracle 2, relay 2 | 13 | `excluded`, "plugin lives in another repo" |
201
+ | In-repo but unemitted — curator 11, tribunal 6, archivist 5, governor 4, historian 3, librarian 2, compass 2, and three singles | 36 | `excluded`, with the specific reason |
202
+ | `onlooker.session.summary` | 1 | `excluded`, emitted by the agent, not this repo |
203
+
204
+ Curator's eleven are deferred by design; compass has no implementation at all
205
+ and is still in its design phase. Those move to `expected` as the features land,
206
+ which is the ratchet working as intended.
207
+
208
+ Note that source reference is not emission. The initial `expected` list is
209
+ seeded from what the first instrumented run actually observes, not from the 75
210
+ grep hits — a type named only in a comment or a doc would otherwise be demanded
211
+ to have an example it can never produce.
212
+
213
+ ## Testing the harness itself
214
+
215
+ Three checks, each broken on purpose before being trusted:
216
+
217
+ 1. Feed a known-bad payload through the emitter with the report directory set;
218
+ confirm a `valid:false` line appears and Gate A goes red.
219
+ 2. Remove a type from `expected` that the suite does emit; confirm assertion 2
220
+ fails on the resulting gap.
221
+ 3. Simulate the schema package being unresolvable; confirm Gate A fails on the
222
+ absence of any `validated:true` line rather than passing green.
223
+
224
+ Check 3 is the one that matters most, because it is the only one that tests the
225
+ harness against its own failure mode.
226
+
227
+ ## Out of scope
228
+
229
+ - **Refactoring fifteen plugins to extract payload builders.** The integration
230
+ seam makes it unnecessary.
231
+ - **Forcing coverage of the 36 unemitted in-repo types now.** They are
232
+ excluded with reasons and promoted as features land.
233
+ - **Parallel bats.** The suite runs serially. Running `bats -j` would need
234
+ append-atomicity revisited; not addressed here.
235
+ - **The `-S error` shellcheck severity question.** Unrelated, separately filed.
236
+
237
+ ## Acceptance
238
+
239
+ - `npm run test:ci` runs `test:bus` after `test:bats` and fails when the report
240
+ is missing.
241
+ - Gate A fails on a rejected emission, naming the type and the ajv errors.
242
+ - Gate A fails when validation never ran, rather than passing vacuously.
243
+ - Gate B fails when a registered type appears in neither manifest list.
244
+ - `ecosystem-ci0`'s `finding_type:"unknown"` payload is caught by Gate A once
245
+ a test exercises that branch, without a bespoke test being written for it.
246
+ - Production emission behavior is unchanged: no new dependency, no exit-code
247
+ change, and nothing written unless `ONLOOKER_TEST_REPORT_DIR` is set.
248
+
249
+ ## Documentation to update
250
+
251
+ - `docs/architecture.md` — how the bus gates fit the event pipeline.
252
+ - `CLAUDE.md` — the "Adding a new plugin" checklist gains a step: triage any
253
+ new event type into `test/bus-coverage.json`.
254
+ - `.claude/skills/writing-tests/SKILL.md` — note that emissions are gated
255
+ suite-wide, so new plugins need no bespoke schema test.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.43.2",
3
+ "version": "0.43.3",
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",
@@ -20,16 +20,18 @@
20
20
  },
21
21
  "scripts": {
22
22
  "test": "npm run test:bats && npm run test:schema",
23
- "test:bats": "bats test/bats",
24
- "test:schema": "node --test test/node/*.test.mjs",
23
+ "test:bats": "rm -rf \"$PWD/test/tmp-emission-report\" && ONLOOKER_TEST_REPORT_DIR=\"$PWD/test/tmp-emission-report\" bats test/bats",
24
+ "test:schema": "ONLOOKER_TEST_REPORT_DIR=\"$PWD/test/tmp-emission-report\" node --test test/node/*.test.mjs",
25
+ "test:bus": "node scripts/lint/check-bus-coverage.mjs",
25
26
  "test:shellcheck": "git ls-files -z '*.sh' | xargs -0 shellcheck -S error -x",
26
27
  "lint:references": "node scripts/lint/check-references.mjs",
27
28
  "lint:manifests": "node scripts/lint/check-manifests.mjs",
28
29
  "lint:lesson-schema": "node scripts/lint/check-lesson-schema-drift.mjs",
30
+ "lint:managed-blocks": "node scripts/lint/check-managed-blocks.mjs",
29
31
  "coverage:node": "node scripts/coverage/run-coverage.mjs",
30
32
  "coverage:bash": "node scripts/coverage/bash-coverage.mjs",
31
33
  "coverage": "npm run coverage:node && npm run coverage:bash",
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",
34
+ "test:ci": "npm run test:shellcheck && npm run test:bats && npm run test:schema && npm run test:bus && npm run lint:managed-blocks && npm run lint:check && npm run lint:manifests && npm run lint:references && npm run lint:lesson-schema",
33
35
  "lint:check": "biome check . && markdownlint '**/*.md'",
34
36
  "lint": "biome lint --write && markdownlint --fix '**/*.md'",
35
37
  "format": "biome format --write && markdownlint --fix '**/*.md'",
@@ -4,7 +4,7 @@
4
4
  * Uses @onlooker-community/schema for envelope shape and validation.
5
5
  */
6
6
  import { randomUUID } from 'node:crypto';
7
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
7
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
 
10
10
  // Canonical event-type constants, inlined rather than imported from
@@ -105,6 +105,41 @@ async function tryValidate(event) {
105
105
  return { available: true, ...schema.validate(event) };
106
106
  }
107
107
 
108
+ /**
109
+ * Test-only emission report.
110
+ *
111
+ * When ONLOOKER_TEST_REPORT_DIR is set, append one line per emission recording
112
+ * the event type, whether validation actually ran, and whether it passed.
113
+ *
114
+ * This exists because both signals the emitter produces on rejection — a
115
+ * non-zero exit and stderr — are destroyed by the hook's fail-soft exit 0,
116
+ * which leaves a dropped event indistinguishable from one that never fired.
117
+ * The report lives outside the per-test BATS_TEST_TMPDIR so the suite can gate
118
+ * on it after the fact.
119
+ *
120
+ * Production never sets the variable, so nothing is written there and the
121
+ * fail-open contract in ADR-005 is untouched. `validated` is recorded
122
+ * separately from `valid` so a run where the schema package never resolved is
123
+ * distinguishable from a run where everything passed — without it, a missing
124
+ * node_modules would make the gate pass while checking nothing.
125
+ */
126
+ function recordEmission(event, check) {
127
+ const dir = process.env.ONLOOKER_TEST_REPORT_DIR;
128
+ if (!dir) return;
129
+ const record = {
130
+ event_type: event?.event_type ?? null,
131
+ validated: check.available === true,
132
+ valid: check.available === true ? check.valid === true : null,
133
+ };
134
+ if (check.available && !check.valid) record.errors = check.errors;
135
+ try {
136
+ mkdirSync(dir, { recursive: true });
137
+ appendFileSync(join(dir, 'emissions.jsonl'), `${JSON.stringify(record)}\n`);
138
+ } catch {
139
+ // A broken report must never break an emission.
140
+ }
141
+ }
142
+
108
143
  function summarizeText(value, maxLen = 1000) {
109
144
  if (value == null) return undefined;
110
145
  const text = String(value).replace(/\s+/g, ' ').trim();
@@ -510,6 +545,7 @@ async function main() {
510
545
  // Best-effort: reject when a validator is present (dev/CI), fail open in
511
546
  // installed plugins so the event is still emitted.
512
547
  const check = await tryValidate(mapped.event);
548
+ recordEmission(mapped.event, check);
513
549
  if (check.available && !check.valid) {
514
550
  console.error(JSON.stringify(check.errors, null, 2));
515
551
  process.exit(1);
@@ -538,6 +574,7 @@ async function main() {
538
574
  // Best-effort: reject when a validator is present (dev/CI), fail open in
539
575
  // installed plugins so the event is still emitted.
540
576
  const check = await tryValidate(event);
577
+ recordEmission(event, check);
541
578
  if (check.available && !check.valid) {
542
579
  console.error(JSON.stringify(check.errors, null, 2));
543
580
  process.exit(1);
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ // Bus coverage gates.
3
+ //
4
+ // Gate A: no emission recorded during the suite was rejected by the schema.
5
+ //
6
+ // Reads the report the emitter writes when ONLOOKER_TEST_REPORT_DIR is set —
7
+ // see recordEmission in scripts/lib/onlooker-event.mjs. A rejected emission is
8
+ // invisible any other way: the emitter exits 1 and prints ajv errors, and the
9
+ // hook's fail-soft exit 0 destroys both.
10
+ //
11
+ // Gate B: every registered event type is accounted for — either it produced
12
+ // a validated emission during the suite, or the manifest excuses it with a
13
+ // stated reason. See test/bus-coverage.json.
14
+ //
15
+ // Exit codes:
16
+ // 0 ok
17
+ // 1 gate A or gate B failure
18
+ // 2 unknown argument
19
+ //
20
+ // Usage: check-bus-coverage.mjs [--report <dir>] [--manifest <path>]
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { dirname, join, resolve } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const HERE = dirname(fileURLToPath(import.meta.url));
26
+ const REPO_ROOT = resolve(HERE, '..', '..');
27
+
28
+ function parseArgs(argv) {
29
+ const out = {
30
+ report: join(REPO_ROOT, 'test', 'tmp-emission-report'),
31
+ manifest: join(REPO_ROOT, 'test', 'bus-coverage.json'),
32
+ };
33
+ for (let i = 2; i < argv.length; i += 1) {
34
+ const a = argv[i];
35
+ if (a === '--report') out.report = argv[++i];
36
+ else if (a === '--manifest') out.manifest = argv[++i];
37
+ else if (a === '--help') {
38
+ process.stderr.write('Usage: check-bus-coverage.mjs [--report <dir>] [--manifest <path>]\n');
39
+ process.exit(0);
40
+ } else {
41
+ process.stderr.write(`check-bus-coverage: unknown argument: ${a}\n`);
42
+ process.exit(2);
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+
48
+ function loadReport(dir) {
49
+ const p = join(dir, 'emissions.jsonl');
50
+ if (!existsSync(p)) return [];
51
+ return readFileSync(p, 'utf8')
52
+ .trim()
53
+ .split('\n')
54
+ .filter(Boolean)
55
+ .map((l) => JSON.parse(l));
56
+ }
57
+
58
+ function gateA(lines) {
59
+ const failures = [];
60
+ if (lines.length === 0) {
61
+ failures.push('no emissions recorded — run `npm run test:bats` with ONLOOKER_TEST_REPORT_DIR set');
62
+ return failures;
63
+ }
64
+ if (!lines.some((l) => l.validated === true)) {
65
+ failures.push(
66
+ 'no emission was validated: @onlooker-community/schema did not resolve, so this gate ' +
67
+ 'checked nothing. Run `npm ci` and try again.',
68
+ );
69
+ }
70
+ for (const l of lines.filter((x) => x.valid === false)) {
71
+ failures.push(`rejected emission: ${l.event_type} — ${JSON.stringify(l.errors)}`);
72
+ }
73
+ return failures;
74
+ }
75
+
76
+ /**
77
+ * Gate B: every registered event type is accounted for.
78
+ *
79
+ * `expected` types must have produced a validated emission during the suite.
80
+ * `excluded` types must carry a reason and must NOT have been emitted —
81
+ * otherwise coverage is silently under-claimed and the manifest can drift
82
+ * downward without CI noticing. Together `expected` and `excluded` must
83
+ * equal ALL_EVENT_TYPES exactly, so a newly registered type belongs to
84
+ * neither and fails here until someone triages it deliberately.
85
+ */
86
+ async function gateB(lines, manifestPath) {
87
+ const failures = [];
88
+ let schema;
89
+ try {
90
+ schema = await import('@onlooker-community/schema');
91
+ } catch {
92
+ return ['@onlooker-community/schema is not installed; run `npm ci`'];
93
+ }
94
+ const registered = new Set(schema.ALL_EVENT_TYPES);
95
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
96
+ const expected = manifest.expected ?? [];
97
+ const excluded = manifest.excluded ?? {};
98
+
99
+ const emitted = new Set(lines.filter((l) => l.valid === true).map((l) => l.event_type));
100
+ for (const t of expected) {
101
+ if (!emitted.has(t)) failures.push(`expected type never emitted during the suite: ${t}`);
102
+ }
103
+
104
+ const accounted = new Set([...expected, ...Object.keys(excluded)]);
105
+ for (const t of registered) {
106
+ if (!accounted.has(t)) {
107
+ failures.push(`registered type is in neither list — triage it in the manifest: ${t}`);
108
+ }
109
+ }
110
+ for (const t of accounted) {
111
+ if (!registered.has(t)) {
112
+ failures.push(`manifest names a type the schema does not register: ${t}`);
113
+ }
114
+ }
115
+ for (const [t, reason] of Object.entries(excluded)) {
116
+ if (!reason || !String(reason).trim()) {
117
+ failures.push(`excluded type needs a reason: ${t}`);
118
+ }
119
+ }
120
+ // An excluded type that is actually emitted and valid is coverage silently
121
+ // under-claimed: the manifest says "nothing tests this" while the suite
122
+ // does. Catch it before the manifest can drift downward unnoticed.
123
+ for (const t of Object.keys(excluded)) {
124
+ if (emitted.has(t)) {
125
+ failures.push(`excluded type is actually emitted — move it to expected: ${t}`);
126
+ }
127
+ }
128
+ return failures;
129
+ }
130
+
131
+ async function main() {
132
+ const args = parseArgs(process.argv);
133
+ const lines = loadReport(args.report);
134
+ const failures = gateA(lines);
135
+ // Skip Gate B unless something was genuinely validated. A merely
136
+ // non-empty report where nothing validated (schema package never
137
+ // resolved) would otherwise bury the one real failure under a spurious
138
+ // "expected type never emitted" line for every expected type.
139
+ if (lines.some((l) => l.validated === true)) failures.push(...(await gateB(lines, args.manifest)));
140
+ if (failures.length) {
141
+ for (const f of failures) process.stderr.write(`check-bus-coverage: ${f}\n`);
142
+ process.exit(1);
143
+ }
144
+ process.stdout.write(`check-bus-coverage: ok (${lines.length} emission(s))\n`);
145
+ }
146
+
147
+ const isMain = process.argv[1]?.endsWith('check-bus-coverage.mjs') ?? false;
148
+ if (isMain) {
149
+ main().catch((err) => {
150
+ process.stderr.write(`check-bus-coverage: ${err.message}\n`);
151
+ process.exit(1);
152
+ });
153
+ }