@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.
- package/.claude/settings.json +1 -1
- package/.claude/skills/writing-tests/SKILL.md +18 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +19 -6
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +18 -3
- package/biome.json +7 -1
- package/docs/architecture.md +41 -1
- package/docs/superpowers/plans/2026-08-21-schema-emission-harness.md +1115 -0
- package/docs/superpowers/specs/2026-08-21-schema-emission-harness-design.md +255 -0
- package/package.json +6 -4
- package/scripts/lib/onlooker-event.mjs +38 -1
- package/scripts/lint/check-bus-coverage.mjs +153 -0
- package/scripts/lint/check-managed-blocks.mjs +206 -0
- package/test/bats/assayer-events.bats +1 -1
- package/test/bats/bursar-events.bats +1 -1
- package/test/bats/cartographer-events.bats +2 -2
- package/test/bats/echo-events.bats +1 -1
- package/test/bats/emission-report-optout.bats +41 -0
- package/test/bats/governor-events.bats +1 -1
- package/test/bats/inspector-events.bats +1 -1
- package/test/bats/librarian-session-end.bats +1 -1
- package/test/bats/lineage-events.bats +2 -2
- package/test/bats/tribunal-events.bats +1 -1
- package/test/bats/warden-events.bats +1 -1
- package/test/bus-coverage.json +131 -0
- package/test/helpers/setup.bash +22 -0
- package/test/node/check-bus-coverage.test.mjs +160 -0
- package/test/node/check-managed-blocks.test.mjs +179 -0
- package/test/node/emission-report.test.mjs +97 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Managed-block fence checker.
|
|
3
|
+
//
|
|
4
|
+
// Tools like `bd setup` own the text between a `<!-- BEGIN ... -->` and
|
|
5
|
+
// `<!-- END ... -->` marker pair and rewrite it verbatim on every run. Any
|
|
6
|
+
// `markdownlint --fix` we apply inside such a block is silently undone the next
|
|
7
|
+
// time anyone regenerates it. Because CI runs `lint:check` (markdownlint with
|
|
8
|
+
// no `--fix`), that churn lands as a red build that looks unrelated to whatever
|
|
9
|
+
// the person was actually doing.
|
|
10
|
+
//
|
|
11
|
+
// The fix is to fence each block with markdownlint-disable/enable comments
|
|
12
|
+
// placed *outside* the BEGIN/END markers, so the generator cannot clobber them.
|
|
13
|
+
// This script asserts that every managed block in every tracked markdown file
|
|
14
|
+
// carries such a fence, covering the rules the generated text is known to trip.
|
|
15
|
+
//
|
|
16
|
+
// Exit codes:
|
|
17
|
+
// 0 ok
|
|
18
|
+
// 1 one or more unfenced or malformed managed blocks
|
|
19
|
+
// 2 setup/usage error
|
|
20
|
+
//
|
|
21
|
+
// Flags:
|
|
22
|
+
// --root <path> override the repo root (used by the tests)
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { dirname, join, resolve } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
// Rules the `bd`-generated blocks trip. MD034 fires because the generator
|
|
30
|
+
// writes bare URLs where markdownlint wants angle brackets; MD012 because it
|
|
31
|
+
// leaves consecutive blank lines; MD024 because two different `bd` subcommands
|
|
32
|
+
// each emit a "Beads Issue Tracker" heading into the same file.
|
|
33
|
+
const REQUIRED_RULES = ['MD012', 'MD024', 'MD034'];
|
|
34
|
+
|
|
35
|
+
const BEGIN_MARKER = /^<!--\s*BEGIN\s+\S.*-->\s*$/;
|
|
36
|
+
const END_MARKER = /^<!--\s*END\s+\S.*-->\s*$/;
|
|
37
|
+
const DISABLE_COMMENT = /^<!--\s*markdownlint-disable\s+(.*?)\s*-->\s*$/;
|
|
38
|
+
const ENABLE_COMMENT = /^<!--\s*markdownlint-enable\s+(.*?)\s*-->\s*$/;
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const args = { root: null };
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
if (argv[i] === '--root') {
|
|
44
|
+
args.root = argv[++i];
|
|
45
|
+
if (!args.root) usageError('--root requires a path');
|
|
46
|
+
} else {
|
|
47
|
+
usageError(`unknown argument: ${argv[i]}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function usageError(message) {
|
|
54
|
+
process.stderr.write(`check-managed-blocks: ${message}\n`);
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Minimal .markdownlintignore support, matching the two pattern shapes the file
|
|
59
|
+
// actually uses: a directory prefix ("node_modules/") and a recursive basename
|
|
60
|
+
// glob ("**/CHANGELOG.md"). Anything else is compared literally. Kept
|
|
61
|
+
// deliberately small -- if the ignore file grows richer patterns, reach for a
|
|
62
|
+
// real glob matcher rather than extending this.
|
|
63
|
+
function loadIgnorePatterns(root) {
|
|
64
|
+
const ignorePath = join(root, '.markdownlintignore');
|
|
65
|
+
if (!existsSync(ignorePath)) return [];
|
|
66
|
+
return readFileSync(ignorePath, 'utf8')
|
|
67
|
+
.split('\n')
|
|
68
|
+
.map((line) => line.trim())
|
|
69
|
+
.filter((line) => line !== '' && !line.startsWith('#'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isIgnored(relPath, patterns) {
|
|
73
|
+
for (const pattern of patterns) {
|
|
74
|
+
if (pattern.endsWith('/')) {
|
|
75
|
+
if (relPath === pattern.slice(0, -1) || relPath.startsWith(pattern)) return true;
|
|
76
|
+
} else if (pattern.startsWith('**/')) {
|
|
77
|
+
const basename = pattern.slice(3);
|
|
78
|
+
if (relPath === basename || relPath.endsWith(`/${basename}`)) return true;
|
|
79
|
+
} else if (relPath === pattern) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function trackedMarkdownFiles(root) {
|
|
87
|
+
let stdout;
|
|
88
|
+
try {
|
|
89
|
+
stdout = execFileSync('git', ['ls-files', '-z', '*.md'], { cwd: root, encoding: 'utf8' });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
usageError(`could not list tracked files under ${root}: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
return stdout.split('\0').filter((path) => path !== '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function rulesFrom(match) {
|
|
97
|
+
return match[1].split(/\s+/).filter((rule) => rule !== '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function missingRules(declared) {
|
|
101
|
+
return REQUIRED_RULES.filter((rule) => !declared.includes(rule));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Walks one file and reports every managed block that is not correctly fenced.
|
|
105
|
+
function checkFile(relPath, contents, errors) {
|
|
106
|
+
const lines = contents.split('\n');
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
if (!BEGIN_MARKER.test(lines[i])) continue;
|
|
110
|
+
|
|
111
|
+
const beginLine = lines[i];
|
|
112
|
+
const beginNo = i + 1;
|
|
113
|
+
const label = `${relPath}:${beginNo}`;
|
|
114
|
+
|
|
115
|
+
// Locate the matching END before judging the fence, so an unterminated
|
|
116
|
+
// block is reported as exactly one error rather than cascading.
|
|
117
|
+
let endIndex = -1;
|
|
118
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
119
|
+
if (BEGIN_MARKER.test(lines[j])) break; // nested/overlapping: stop looking
|
|
120
|
+
if (END_MARKER.test(lines[j])) {
|
|
121
|
+
endIndex = j;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (endIndex === -1) {
|
|
127
|
+
errors.push(`${label}: managed block "${beginLine.trim()}" has no matching <!-- END ... --> marker`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const before = i > 0 ? lines[i - 1] : '';
|
|
132
|
+
const disable = before.match(DISABLE_COMMENT);
|
|
133
|
+
if (!disable) {
|
|
134
|
+
errors.push(
|
|
135
|
+
`${label}: managed block is not fenced -- the line directly above it must be ` +
|
|
136
|
+
`"<!-- markdownlint-disable ${REQUIRED_RULES.join(' ')} -->", found ${JSON.stringify(before)}`,
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
const missing = missingRules(rulesFrom(disable));
|
|
140
|
+
if (missing.length > 0) {
|
|
141
|
+
errors.push(`${label}: markdownlint-disable above this block omits ${missing.join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const after = endIndex + 1 < lines.length ? lines[endIndex + 1] : '';
|
|
146
|
+
const enable = after.match(ENABLE_COMMENT);
|
|
147
|
+
if (!enable) {
|
|
148
|
+
errors.push(
|
|
149
|
+
`${relPath}:${endIndex + 1}: managed block is not closed -- the line directly below its END marker ` +
|
|
150
|
+
`must be "<!-- markdownlint-enable ${REQUIRED_RULES.join(' ')} -->", found ${JSON.stringify(after)}`,
|
|
151
|
+
);
|
|
152
|
+
} else {
|
|
153
|
+
const missing = missingRules(rulesFrom(enable));
|
|
154
|
+
if (missing.length > 0) {
|
|
155
|
+
errors.push(`${relPath}:${endIndex + 2}: markdownlint-enable below this block omits ${missing.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
i = endIndex;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function main() {
|
|
164
|
+
const args = parseArgs(process.argv.slice(2));
|
|
165
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
166
|
+
const root = resolve(args.root ?? join(here, '..', '..'));
|
|
167
|
+
|
|
168
|
+
if (!existsSync(root)) usageError(`root does not exist: ${root}`);
|
|
169
|
+
|
|
170
|
+
const ignorePatterns = loadIgnorePatterns(root);
|
|
171
|
+
const errors = [];
|
|
172
|
+
let blockCount = 0;
|
|
173
|
+
let fileCount = 0;
|
|
174
|
+
|
|
175
|
+
for (const relPath of trackedMarkdownFiles(root)) {
|
|
176
|
+
if (isIgnored(relPath, ignorePatterns)) continue;
|
|
177
|
+
|
|
178
|
+
const absPath = join(root, relPath);
|
|
179
|
+
if (!existsSync(absPath)) continue; // staged deletion; nothing to check
|
|
180
|
+
|
|
181
|
+
const contents = readFileSync(absPath, 'utf8');
|
|
182
|
+
if (!contents.includes('<!--')) continue; // fast path: no comments, no markers
|
|
183
|
+
|
|
184
|
+
checkFile(relPath, contents, errors);
|
|
185
|
+
|
|
186
|
+
const found = (contents.match(new RegExp(BEGIN_MARKER.source, 'gm')) ?? []).length;
|
|
187
|
+
if (found > 0) {
|
|
188
|
+
fileCount++;
|
|
189
|
+
blockCount += found;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const e of errors) process.stderr.write(`error: ${e}\n`);
|
|
194
|
+
|
|
195
|
+
if (errors.length > 0) {
|
|
196
|
+
process.stderr.write(
|
|
197
|
+
`check-managed-blocks: ${errors.length} error(s) across ${blockCount} managed block(s)\n` +
|
|
198
|
+
'Fence generated blocks so `bd setup` cannot undo `markdownlint --fix`. See ecosystem-55g.\n',
|
|
199
|
+
);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
process.stdout.write(`check-managed-blocks: ok (${blockCount} managed block(s) in ${fileCount} file(s))\n`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main();
|
|
@@ -89,7 +89,7 @@ AUDIT_ID="01J0000000000000000000AB34"
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
@test "emission fails on unknown event type" {
|
|
92
|
-
|
|
92
|
+
expect_emission_rejected assayer_emit_event "assayer.no.such.event" '{"audit_id":"x"}'
|
|
93
93
|
[ "$status" -ne 0 ]
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -68,6 +68,6 @@ _validate_latest_event() {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
@test "bursar_emit_event returns nonzero for an unknown event type" {
|
|
71
|
-
|
|
71
|
+
expect_emission_rejected bursar_emit_event "bursar.no_such_event" '{"project_key":"x"}' "$SID"
|
|
72
72
|
[ "$status" -ne 0 ]
|
|
73
73
|
}
|
|
@@ -147,7 +147,7 @@ _finding() {
|
|
|
147
147
|
_require_cartographer_schema
|
|
148
148
|
# Guards the direction of the fix: if someone "restores" the old schema,
|
|
149
149
|
# this is the test that objects.
|
|
150
|
-
|
|
150
|
+
expect_emission_rejected cartographer_emit_event "cartographer.issue.found" \
|
|
151
151
|
'{"issue_type":"orphaned_plugin","file_path":"CLAUDE.md","severity":"warning"}'
|
|
152
152
|
[ "$status" -ne 0 ]
|
|
153
153
|
}
|
|
@@ -257,7 +257,7 @@ _finding() {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
@test "emission fails on unknown event type" {
|
|
260
|
-
|
|
260
|
+
expect_emission_rejected cartographer_emit_event "cartographer.no.such.event" '{"audit_id":"x"}'
|
|
261
261
|
[ "$status" -ne 0 ]
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -111,7 +111,7 @@ TEST_ID="01J000000000000000000000TT"
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
@test "emission fails on unknown event type" {
|
|
114
|
-
|
|
114
|
+
expect_emission_rejected echo_emit_event "echo.no.such.event" '{"suite_id":"x"}'
|
|
115
115
|
[ "$status" -ne 0 ]
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/warden"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
export ONLOOKER_ECOSYSTEM_ROOT="$REPO_ROOT"
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/warden-events.sh"
|
|
11
|
+
|
|
12
|
+
export ONLOOKER_TEST_REPORT_DIR="${BATS_TEST_TMPDIR}/report"
|
|
13
|
+
mkdir -p "$ONLOOKER_TEST_REPORT_DIR"
|
|
14
|
+
REPORT="${ONLOOKER_TEST_REPORT_DIR}/emissions.jsonl"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_valid_payload() {
|
|
18
|
+
jq -cn '{source_type:"web_fetch", threat_type:"prompt_injection", confidence:0.5}'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Positive control. Without this, the opt-out test below could pass because
|
|
22
|
+
# nothing writes a report at all, rather than because the helper suppressed it.
|
|
23
|
+
@test "a normal emission does write a report line" {
|
|
24
|
+
run warden_emit_event "warden.threat.detected" "$(_valid_payload)"
|
|
25
|
+
[ "$status" -eq 0 ] || return 1
|
|
26
|
+
[ -s "$REPORT" ]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@test "expect_emission_rejected keeps a deliberate rejection out of the report" {
|
|
30
|
+
expect_emission_rejected warden_emit_event "warden.bogus.event" "$(_valid_payload)"
|
|
31
|
+
[ "$status" -ne 0 ] || return 1
|
|
32
|
+
[ ! -f "$REPORT" ]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@test "expect_emission_rejected restores the report dir afterward" {
|
|
36
|
+
expect_emission_rejected warden_emit_event "warden.bogus.event" "$(_valid_payload)"
|
|
37
|
+
[ "$ONLOOKER_TEST_REPORT_DIR" = "${BATS_TEST_TMPDIR}/report" ] || return 1
|
|
38
|
+
run warden_emit_event "warden.threat.detected" "$(_valid_payload)"
|
|
39
|
+
[ "$status" -eq 0 ] || return 1
|
|
40
|
+
[ -s "$REPORT" ]
|
|
41
|
+
}
|
|
@@ -233,6 +233,6 @@ AID="bats-agent-000"
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
@test "governor_emit_event returns nonzero for unknown event type" {
|
|
236
|
-
|
|
236
|
+
expect_emission_rejected governor_emit_event "governor.no_such_event" '{"session_id":"x"}'
|
|
237
237
|
[ "$status" -ne 0 ]
|
|
238
238
|
}
|
|
@@ -146,7 +146,7 @@ _validate_latest_event() {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
@test "emission rejects an unknown event type" {
|
|
149
|
-
|
|
149
|
+
expect_emission_rejected inspector_emit_event "inspector.no.such.event" '{"file_path":"x"}'
|
|
150
150
|
[ "$status" -ne 0 ]
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -329,7 +329,7 @@ _scan_complete() {
|
|
|
329
329
|
source "${PLUGIN_ROOT}/scripts/lib/librarian-emit.sh"
|
|
330
330
|
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
331
331
|
|
|
332
|
-
librarian_emit "librarian.scan.complete" "sess-bad" \
|
|
332
|
+
expect_emission_rejected librarian_emit "librarian.scan.complete" "sess-bad" \
|
|
333
333
|
'{"outcome":"gave_up","duration_ms":5}'
|
|
334
334
|
|
|
335
335
|
# A refused payload never reaches the log, so the file may not exist at all.
|
|
@@ -71,11 +71,11 @@ _validate_latest_event() {
|
|
|
71
71
|
local p
|
|
72
72
|
p=$(jq -n --arg pk "$PK" --arg sid "$SID" \
|
|
73
73
|
'{project_key:$pk, session_id:$sid, file_path:"x", tool:"NotebookEdit", operation:"edit"}')
|
|
74
|
-
|
|
74
|
+
expect_emission_rejected lineage_emit_event "lineage.change.recorded" "$p" "$SID"
|
|
75
75
|
[ "$status" -ne 0 ]
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
@test "lineage_emit_event returns nonzero for an unknown event type" {
|
|
79
|
-
|
|
79
|
+
expect_emission_rejected lineage_emit_event "lineage.no_such_event" '{"project_key":"x"}' "$SID"
|
|
80
80
|
[ "$status" -ne 0 ]
|
|
81
81
|
}
|
|
@@ -204,6 +204,6 @@ JUDGE_ID="01J000000000000000000000JJ"
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
@test "emission fails loudly on bogus event_type (schema rejects)" {
|
|
207
|
-
|
|
207
|
+
expect_emission_rejected tribunal_emit_event "tribunal.no.such.event" '{"task_id":"x"}'
|
|
208
208
|
[ "$status" -ne 0 ]
|
|
209
209
|
}
|
|
@@ -80,6 +80,6 @@ _validate_latest_event() {
|
|
|
80
80
|
# warden.* type must be rejected so typos never reach the log.
|
|
81
81
|
local p
|
|
82
82
|
p=$(jq -n '{source_type:"web_fetch", threat_type:"prompt_injection", confidence:0.5}')
|
|
83
|
-
|
|
83
|
+
expect_emission_rejected warden_emit_event "warden.bogus.event" "$p"
|
|
84
84
|
[ "$status" -ne 0 ]
|
|
85
85
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expected": [
|
|
3
|
+
"assayer.audit.complete",
|
|
4
|
+
"assayer.audit.started",
|
|
5
|
+
"assayer.claim.contradicted",
|
|
6
|
+
"assayer.claim.unverified",
|
|
7
|
+
"bursar.rollup.skipped",
|
|
8
|
+
"bursar.rollup.surfaced",
|
|
9
|
+
"bursar.session.recorded",
|
|
10
|
+
"cartographer.audit.complete",
|
|
11
|
+
"cartographer.issue.found",
|
|
12
|
+
"cartographer.issue.resolved",
|
|
13
|
+
"compass.check.passed",
|
|
14
|
+
"compass.check.skipped",
|
|
15
|
+
"counsel.brief.generated",
|
|
16
|
+
"curator.finding.broken_index",
|
|
17
|
+
"curator.finding.date_decayed",
|
|
18
|
+
"curator.finding.orphaned_memory",
|
|
19
|
+
"curator.finding.path_broken",
|
|
20
|
+
"curator.scan.complete",
|
|
21
|
+
"curator.scan.started",
|
|
22
|
+
"echo.improvement.detected",
|
|
23
|
+
"echo.regression.detected",
|
|
24
|
+
"echo.suite.complete",
|
|
25
|
+
"echo.suite.started",
|
|
26
|
+
"governor.budget.exceeded",
|
|
27
|
+
"governor.budget.warning",
|
|
28
|
+
"governor.call.recorded",
|
|
29
|
+
"governor.child.allocated",
|
|
30
|
+
"governor.child.returned",
|
|
31
|
+
"governor.gate.checked",
|
|
32
|
+
"governor.ledger.write_failed",
|
|
33
|
+
"governor.lock.stale_cleared",
|
|
34
|
+
"governor.session.complete",
|
|
35
|
+
"historian.chunk.dropped",
|
|
36
|
+
"historian.chunk.sanitized",
|
|
37
|
+
"historian.embedder.unavailable",
|
|
38
|
+
"historian.indexing.complete",
|
|
39
|
+
"historian.indexing.started",
|
|
40
|
+
"historian.retrieval.complete",
|
|
41
|
+
"historian.retrieval.started",
|
|
42
|
+
"historian.retrieval.surfaced",
|
|
43
|
+
"inspector.check.failed",
|
|
44
|
+
"inspector.check.passed",
|
|
45
|
+
"inspector.check.skipped",
|
|
46
|
+
"inspector.run.completed",
|
|
47
|
+
"librarian.candidate.dropped",
|
|
48
|
+
"librarian.candidate.proposed",
|
|
49
|
+
"librarian.proposal.accepted",
|
|
50
|
+
"librarian.proposal.rejected",
|
|
51
|
+
"librarian.scan.complete",
|
|
52
|
+
"librarian.scan.started",
|
|
53
|
+
"librarian.tombstone.created",
|
|
54
|
+
"lineage.change.recorded",
|
|
55
|
+
"lineage.query.answered",
|
|
56
|
+
"memory.recalled",
|
|
57
|
+
"onlooker.artifact.ready",
|
|
58
|
+
"session.compact",
|
|
59
|
+
"session.end",
|
|
60
|
+
"session.prompt",
|
|
61
|
+
"session.start",
|
|
62
|
+
"skill.invoked",
|
|
63
|
+
"task.complete",
|
|
64
|
+
"task.start",
|
|
65
|
+
"tool.agent.spawn",
|
|
66
|
+
"tool.file.read",
|
|
67
|
+
"tool.shell.exec",
|
|
68
|
+
"tribunal.actor.complete",
|
|
69
|
+
"tribunal.actor.start",
|
|
70
|
+
"tribunal.consensus.reached",
|
|
71
|
+
"tribunal.dissent.recorded",
|
|
72
|
+
"tribunal.gate.blocked",
|
|
73
|
+
"tribunal.gate.passed",
|
|
74
|
+
"tribunal.iteration.start",
|
|
75
|
+
"tribunal.judge.start",
|
|
76
|
+
"tribunal.jury.empaneled",
|
|
77
|
+
"tribunal.meta.complete",
|
|
78
|
+
"tribunal.meta.start",
|
|
79
|
+
"tribunal.session.complete",
|
|
80
|
+
"tribunal.session.start",
|
|
81
|
+
"tribunal.verdict",
|
|
82
|
+
"warden.gate.blocked",
|
|
83
|
+
"warden.threat.cleared",
|
|
84
|
+
"warden.threat.detected"
|
|
85
|
+
],
|
|
86
|
+
"excluded": {
|
|
87
|
+
"archivist.compact.complete": "archivist-extract.sh (the PreCompact hook) never calls archivist_emit_event with this type; only onlooker.artifact.ready is wired up",
|
|
88
|
+
"archivist.compact.started": "archivist-extract.sh (the PreCompact hook) never calls archivist_emit_event with this type; only onlooker.artifact.ready is wired up",
|
|
89
|
+
"archivist.extract.complete": "archivist-extract.sh only emits onlooker.artifact.ready; no code path emits archivist.extract.complete yet",
|
|
90
|
+
"archivist.inject.complete": "archivist-inject.sh (the SessionStart hook) has no archivist_emit_event call at all",
|
|
91
|
+
"archivist.inject.started": "archivist-inject.sh (the SessionStart hook) has no archivist_emit_event call at all",
|
|
92
|
+
"compass.check.canceled": "compass has no implementation yet; design phase",
|
|
93
|
+
"compass.check.failed": "compass has no implementation yet; design phase",
|
|
94
|
+
"compass.check.overridden": "compass has no implementation yet; design phase",
|
|
95
|
+
"curator.finding.acknowledged": "curator check deferred; see plugins/curator/README.md",
|
|
96
|
+
"curator.finding.contradiction": "curator check deferred; see plugins/curator/README.md",
|
|
97
|
+
"curator.finding.redundant_pair": "curator check deferred; see plugins/curator/README.md",
|
|
98
|
+
"curator.finding.resolved": "curator check deferred; see plugins/curator/README.md",
|
|
99
|
+
"curator.finding.symbol_missing": "curator check deferred; see plugins/curator/README.md",
|
|
100
|
+
"curator.finding.unused_low_signal": "curator check deferred; see plugins/curator/README.md",
|
|
101
|
+
"curator.finding.url_unchecked": "curator check deferred; see plugins/curator/README.md",
|
|
102
|
+
"historian.config.warning": "no config-validation-warning branch exists in historian-config.sh; nothing in the plugin emits this type yet",
|
|
103
|
+
"historian.prune.complete": "historian/README.md lists retention-sweep pruning under Status as deferred to a follow-up landing",
|
|
104
|
+
"historian.purge.complete": "historian/README.md lists manual purge under Status as deferred to a follow-up landing",
|
|
105
|
+
"librarian.proposal.merged": "librarian-cli.sh only implements the accept/reject subcommands; the merge-into-X resolution from docs/design.md has no code path",
|
|
106
|
+
"librarian.proposal.superseded": "librarian-cli.sh only implements the accept/reject subcommands; the supersede-X resolution from docs/design.md has no code path",
|
|
107
|
+
"meridian.hint.delivered": "plugin lives in another repo",
|
|
108
|
+
"meridian.hint.generated": "plugin lives in another repo",
|
|
109
|
+
"meridian.lesson.curated": "plugin lives in another repo",
|
|
110
|
+
"meridian.outcome.recorded": "plugin lives in another repo",
|
|
111
|
+
"meridian.playbook.updated": "plugin lives in another repo",
|
|
112
|
+
"meridian.reliance.measured": "plugin lives in another repo",
|
|
113
|
+
"onlooker.session.summary": "emitted by the agent, not this repo",
|
|
114
|
+
"oracle.calibration.complete": "plugin lives in another repo",
|
|
115
|
+
"oracle.calibration.requested": "plugin lives in another repo",
|
|
116
|
+
"prompt_rule.applied": "prompt_rules_emit in scripts/lib/prompt-rules.sh writes straight to $ONLOOKER_EVENTS_LOG via jq, bypassing onlooker-event.mjs, so it never reaches the schema-validating emitter this harness observes even though test/bats/prompt-rules.bats exercises it",
|
|
117
|
+
"prompt_rule.matched": "prompt_rules_emit in scripts/lib/prompt-rules.sh writes straight to $ONLOOKER_EVENTS_LOG via jq, bypassing onlooker-event.mjs, so it never reaches the schema-validating emitter this harness observes even though test/bats/prompt-rules.bats exercises it",
|
|
118
|
+
"relay.handoff.captured": "plugin lives in another repo",
|
|
119
|
+
"relay.handoff.injected": "plugin lives in another repo",
|
|
120
|
+
"scribe.capture.complete": "no scribe_emit_event call for this type exists anywhere in plugins/scribe; only scribe.distill.complete and onlooker.artifact.ready are wired up",
|
|
121
|
+
"scribe.distill.complete": "the emit call in scripts/lib/scribe-distill.sh is real, but test/bats/scribe-extract.bats only drives the min_turns skip path, which returns before reaching it",
|
|
122
|
+
"sentinel.allowed": "plugin lives in another repo",
|
|
123
|
+
"sentinel.blocked": "plugin lives in another repo",
|
|
124
|
+
"sentinel.reviewed": "plugin lives in another repo",
|
|
125
|
+
"task.fail": "mapTaskHookInput in scripts/lib/onlooker-event.mjs only derives task.start/task.complete from TaskCreated/TaskCompleted; no branch distinguishes a failed task",
|
|
126
|
+
"tool.agent.complete": "the Agent/PostToolUse branch in onlooker-event.mjs is real, but test/bats/tool-history-tracker.bats only drives Read and failing-Bash tool inputs, never Agent",
|
|
127
|
+
"tool.file.edit": "the Edit branch in onlooker-event.mjs is real, but test/bats/tool-history-tracker.bats never drives an Edit tool input",
|
|
128
|
+
"tool.file.write": "the Write branch in onlooker-event.mjs is real, but test/bats/tool-history-tracker.bats never drives a Write tool input",
|
|
129
|
+
"tool.web.fetch": "the WebFetch branch in onlooker-event.mjs is real, but test/bats/tool-history-tracker.bats never drives a WebFetch tool input"
|
|
130
|
+
}
|
|
131
|
+
}
|
package/test/helpers/setup.bash
CHANGED
|
@@ -68,3 +68,25 @@ load_validate_path() {
|
|
|
68
68
|
"$ONLOOKER_COMPACT_TRACKERS_DIR" \
|
|
69
69
|
"$ONLOOKER_METRICS_DIR"
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
# Run a command that is expected to fail schema validation, without recording
|
|
73
|
+
# the deliberate rejection in the suite-wide emission report.
|
|
74
|
+
#
|
|
75
|
+
# The report exists so a payload that drifts from the schema turns CI red. A
|
|
76
|
+
# test that deliberately emits an invalid payload would otherwise write a
|
|
77
|
+
# valid:false line indistinguishable from real drift, making the gate
|
|
78
|
+
# permanently red from intentional tests. Unsetting the report directory for
|
|
79
|
+
# the duration keeps the negative test honest — it still asserts the emitter
|
|
80
|
+
# rejects — without polluting the gate.
|
|
81
|
+
#
|
|
82
|
+
# Sets $status and $output exactly as bats' `run` does.
|
|
83
|
+
#
|
|
84
|
+
# Usage: expect_emission_rejected <command> [args...]
|
|
85
|
+
expect_emission_rejected() {
|
|
86
|
+
local saved="${ONLOOKER_TEST_REPORT_DIR:-}"
|
|
87
|
+
unset ONLOOKER_TEST_REPORT_DIR
|
|
88
|
+
run "$@"
|
|
89
|
+
if [ -n "$saved" ]; then
|
|
90
|
+
export ONLOOKER_TEST_REPORT_DIR="$saved"
|
|
91
|
+
fi
|
|
92
|
+
}
|