@onlooker-community/ecosystem 0.9.0 → 0.14.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-plugin/marketplace.json +39 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.github/copilot-instructions.md +46 -0
  4. package/.github/workflows/coverage.yml +78 -0
  5. package/.github/workflows/release.yml +24 -8
  6. package/.github/workflows/test.yml +3 -0
  7. package/.markdownlintignore +3 -0
  8. package/.release-please-manifest.json +4 -1
  9. package/CHANGELOG.md +44 -0
  10. package/README.md +57 -13
  11. package/config.json +6 -1
  12. package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
  13. package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
  14. package/docs/adr/003-ulid-over-uuid.md +40 -0
  15. package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
  16. package/docs/architecture.md +117 -0
  17. package/hooks/hooks.json +4 -0
  18. package/package.json +13 -7
  19. package/plugins/archivist/.claude-plugin/plugin.json +14 -0
  20. package/plugins/archivist/CHANGELOG.md +8 -0
  21. package/plugins/archivist/README.md +105 -0
  22. package/plugins/archivist/config.json +18 -0
  23. package/plugins/archivist/hooks/hooks.json +35 -0
  24. package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
  25. package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
  26. package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
  27. package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
  28. package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
  29. package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
  30. package/plugins/echo/.claude-plugin/plugin.json +14 -0
  31. package/plugins/echo/CHANGELOG.md +24 -0
  32. package/plugins/echo/README.md +110 -0
  33. package/plugins/echo/config.json +15 -0
  34. package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
  35. package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
  36. package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
  37. package/plugins/echo/hooks/hooks.json +15 -0
  38. package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
  39. package/plugins/echo/scripts/lib/echo-config.sh +108 -0
  40. package/plugins/echo/scripts/lib/echo-events.sh +74 -0
  41. package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
  42. package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
  43. package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
  44. package/plugins/tribunal/CHANGELOG.md +10 -0
  45. package/plugins/tribunal/README.md +134 -0
  46. package/plugins/tribunal/agents/tribunal-actor.md +35 -0
  47. package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
  48. package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
  49. package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
  50. package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
  51. package/plugins/tribunal/config.json +50 -0
  52. package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
  53. package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
  54. package/plugins/tribunal/hooks/hooks.json +15 -0
  55. package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
  56. package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
  57. package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
  58. package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
  59. package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
  60. package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
  61. package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
  62. package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
  63. package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
  64. package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
  65. package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
  66. package/release-please-config.json +43 -5
  67. package/scripts/coverage/bash-coverage.mjs +169 -0
  68. package/scripts/coverage/format-comment.mjs +120 -0
  69. package/scripts/coverage/run-coverage.mjs +151 -0
  70. package/scripts/hooks/agent-spawn-tracker.sh +4 -4
  71. package/scripts/hooks/prompt-rule-injector.sh +122 -0
  72. package/scripts/lib/onlooker-event.mjs +82 -10
  73. package/scripts/lib/portable-lock.sh +48 -0
  74. package/scripts/lib/prompt-rules.sh +207 -0
  75. package/scripts/lib/tool-history.sh +7 -8
  76. package/scripts/lib/validate-path.sh +4 -0
  77. package/scripts/lint/check-manifests.mjs +314 -0
  78. package/scripts/lint/check-references.mjs +311 -0
  79. package/skills/list-prompt-rules/SKILL.md +15 -0
  80. package/test/bats/archivist-config-files.bats +60 -0
  81. package/test/bats/archivist-config.bats +54 -0
  82. package/test/bats/archivist-inject.bats +73 -0
  83. package/test/bats/archivist-project-key.bats +75 -0
  84. package/test/bats/archivist-storage.bats +119 -0
  85. package/test/bats/archivist-ulid.bats +36 -0
  86. package/test/bats/config.bats +10 -10
  87. package/test/bats/echo-config.bats +90 -0
  88. package/test/bats/echo-events.bats +121 -0
  89. package/test/bats/echo-project-key.bats +115 -0
  90. package/test/bats/echo-stop-hook.bats +101 -0
  91. package/test/bats/echo-ulid.bats +38 -0
  92. package/test/bats/portable-lock.bats +62 -0
  93. package/test/bats/prompt-rules.bats +269 -0
  94. package/test/bats/read-chunk-tracking.bats +73 -0
  95. package/test/bats/tool-history-tracker.bats +1 -0
  96. package/test/bats/tribunal-aggregate.bats +77 -0
  97. package/test/bats/tribunal-config.bats +86 -0
  98. package/test/bats/tribunal-events.bats +209 -0
  99. package/test/bats/tribunal-gate.bats +95 -0
  100. package/test/bats/tribunal-jury.bats +80 -0
  101. package/test/bats/tribunal-rubric.bats +119 -0
  102. package/test/bats/tribunal-stop-hook.bats +73 -0
  103. package/test/bats/tribunal-verdict.bats +71 -0
  104. package/test/bats/validate-path.bats +1 -1
  105. package/test/fixtures/hook-inputs/post-tool-use-read-chunked.json +15 -0
  106. package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
  107. package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
  108. package/test/helpers/setup.bash +9 -0
  109. package/test/node/check-manifests.test.mjs +173 -0
  110. package/test/node/check-references.test.mjs +279 -0
  111. package/test/node/coverage.test.mjs +143 -0
  112. package/test/node/schema-events.test.mjs +41 -1
@@ -0,0 +1,143 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { describe, it } from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const HERE = dirname(fileURLToPath(import.meta.url));
10
+ const REPO_ROOT = resolve(HERE, '..', '..');
11
+
12
+ function runJson(script, root, ...args) {
13
+ const r = spawnSync('node', [join(REPO_ROOT, script), '--root', root, '--json', ...args], {
14
+ encoding: 'utf8',
15
+ });
16
+ if (r.status !== 0) {
17
+ throw new Error(`${script} exited ${r.status}\nstdout:\n${r.stdout}\nstderr:\n${r.stderr}`);
18
+ }
19
+ return JSON.parse(r.stdout);
20
+ }
21
+
22
+ function scaffoldMarketplace() {
23
+ const root = mkdtempSync(join(tmpdir(), 'coverage-'));
24
+ mkdirSync(join(root, '.claude-plugin'), { recursive: true });
25
+ writeFileSync(
26
+ join(root, '.claude-plugin', 'marketplace.json'),
27
+ JSON.stringify({ name: 't', plugins: [{ name: 's', source: './' }] }),
28
+ );
29
+ return root;
30
+ }
31
+
32
+ function writeFile(path, content) {
33
+ mkdirSync(dirname(path), { recursive: true });
34
+ writeFileSync(path, content);
35
+ }
36
+
37
+ describe('bash-coverage', () => {
38
+ it('returns 100% when every public function is referenced in tests', () => {
39
+ const root = scaffoldMarketplace();
40
+ writeFile(join(root, 'scripts/lib/foo.sh'), `#!/usr/bin/env bash\nfoo() { echo 1; }\nbar() { echo 2; }\n`);
41
+ writeFile(join(root, 'test/bats/foo.bats'), `@test "covers" { foo; bar; }\n`);
42
+ const report = runJson('scripts/coverage/bash-coverage.mjs', root);
43
+ assert.equal(report.overall.total, 2);
44
+ assert.equal(report.overall.tested, 2);
45
+ assert.equal(report.overall.ratio, 1);
46
+ });
47
+
48
+ it('drops untested public functions into the untested list', () => {
49
+ const root = scaffoldMarketplace();
50
+ writeFile(join(root, 'scripts/lib/foo.sh'), `#!/usr/bin/env bash\nfoo() { :; }\nbar() { :; }\nbaz() { :; }\n`);
51
+ writeFile(join(root, 'test/bats/foo.bats'), `@test "covers" { foo; }\n`);
52
+ const report = runJson('scripts/coverage/bash-coverage.mjs', root);
53
+ assert.equal(report.overall.tested, 1);
54
+ assert.equal(report.overall.total, 3);
55
+ const untestedNames = report.untested.map((u) => u.name).sort();
56
+ assert.deepEqual(untestedNames, ['bar', 'baz']);
57
+ });
58
+
59
+ it('skips private (underscore-prefixed) helpers', () => {
60
+ const root = scaffoldMarketplace();
61
+ writeFile(join(root, 'scripts/lib/foo.sh'), `#!/usr/bin/env bash\nfoo() { :; }\n_internal() { :; }\n`);
62
+ writeFile(join(root, 'test/bats/foo.bats'), `@test "covers foo" { foo; }\n`);
63
+ const report = runJson('scripts/coverage/bash-coverage.mjs', root);
64
+ assert.equal(report.overall.total, 1, JSON.stringify(report));
65
+ });
66
+
67
+ it('does not count indented function-looking lines as definitions', () => {
68
+ const root = scaffoldMarketplace();
69
+ writeFile(
70
+ join(root, 'scripts/lib/foo.sh'),
71
+ `#!/usr/bin/env bash\nfoo() {\n inner() { :; } # nested, should NOT count\n}\n`,
72
+ );
73
+ writeFile(join(root, 'test/bats/foo.bats'), `@test "covers" { foo; }\n`);
74
+ const report = runJson('scripts/coverage/bash-coverage.mjs', root);
75
+ assert.equal(report.overall.total, 1);
76
+ });
77
+
78
+ it('handles repos with no .sh files at all', () => {
79
+ const root = scaffoldMarketplace();
80
+ const report = runJson('scripts/coverage/bash-coverage.mjs', root);
81
+ assert.equal(report.overall.total, 0);
82
+ assert.equal(report.overall.ratio, 1);
83
+ });
84
+ });
85
+
86
+ describe('format-comment', () => {
87
+ function render(nodeReport, bashReport, sha = 'abcdef0123456789') {
88
+ const tmp = mkdtempSync(join(tmpdir(), 'cov-fmt-'));
89
+ const nPath = join(tmp, 'node.json');
90
+ const bPath = join(tmp, 'bash.json');
91
+ writeFileSync(nPath, JSON.stringify(nodeReport));
92
+ writeFileSync(bPath, JSON.stringify(bashReport));
93
+ const r = spawnSync(
94
+ 'node',
95
+ [join(REPO_ROOT, 'scripts', 'coverage', 'format-comment.mjs'), '--node', nPath, '--bash', bPath, '--sha', sha],
96
+ { encoding: 'utf8' },
97
+ );
98
+ if (r.status !== 0) throw new Error(`format-comment failed: ${r.stderr}`);
99
+ return r.stdout;
100
+ }
101
+
102
+ it('emits the sentinel comment so the upsert logic can find it', () => {
103
+ const out = render(
104
+ { overall: null, files: [] },
105
+ { overall: { total: 0, tested: 0, ratio: 1 }, files: [], untested: [] },
106
+ );
107
+ assert.ok(out.startsWith('<!-- onlooker-coverage-comment -->'));
108
+ });
109
+
110
+ it('renders node coverage rows in the markdown table', () => {
111
+ const out = render(
112
+ {
113
+ overall: { file: 'all files', line: 75, branch: 55, funcs: 90, uncoveredLines: '' },
114
+ files: [{ file: 'foo.mjs', line: 75, branch: 50, funcs: 80, uncoveredLines: '12-14' }],
115
+ },
116
+ { overall: { total: 1, tested: 1, ratio: 1 }, files: [], untested: [] },
117
+ );
118
+ assert.match(out, /\| `foo\.mjs` \| 75\.0% \| 50\.0% \| 80\.0% \|/);
119
+ // 75% lines → yellow (60–79 inclusive).
120
+ assert.match(out, /\*\*Overall:\*\* 🟡 75\.0% lines/);
121
+ });
122
+
123
+ it('marks bash overall red when below 50%', () => {
124
+ const out = render(
125
+ { overall: null, files: [] },
126
+ { overall: { total: 10, tested: 3, ratio: 0.3 }, files: [], untested: [] },
127
+ );
128
+ assert.match(out, /🔴 3\/10 public functions/);
129
+ });
130
+
131
+ it('lists untested functions inside a collapsed details block', () => {
132
+ const out = render(
133
+ { overall: null, files: [] },
134
+ {
135
+ overall: { total: 2, tested: 1, ratio: 0.5 },
136
+ files: [{ file: 'a.sh', total: 2, tested: 1, ratio: 0.5, untested: ['bar'] }],
137
+ untested: [{ file: 'a.sh', name: 'bar' }],
138
+ },
139
+ );
140
+ assert.match(out, /<details><summary>Untested public functions<\/summary>/);
141
+ assert.match(out, /`a\.sh` — `bar`/);
142
+ });
143
+ });
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { test } from 'node:test';
@@ -7,6 +7,9 @@ import { fileURLToPath } from 'node:url';
7
7
  import { validate } from '@onlooker-community/schema';
8
8
  import {
9
9
  buildCanonicalEvent,
10
+ buildToolFileReadPayload,
11
+ extractReadRange,
12
+ LARGE_FILE_BYTES_ON_DISK,
10
13
  mapHookInputToCanonical,
11
14
  mapSkillHookInput,
12
15
  mapTaskHookInput,
@@ -32,6 +35,43 @@ test('mapHookInputToCanonical maps PostToolUse Read to tool.file.read', () => {
32
35
  assert.equal(mapped.event.event_type, 'tool.file.read');
33
36
  assert.equal(mapped.event.schema_version, '1.0');
34
37
  assert.equal(mapped.event.payload.path, '/project/src/main.ts');
38
+ assert.equal(mapped.event.payload.read_mode, 'full');
39
+ assert.equal(validate(mapped.event).valid, true);
40
+ });
41
+
42
+ test('extractReadRange detects partial reads from offset and limit', () => {
43
+ const range = extractReadRange({ offset: 10, limit: 50 });
44
+ assert.equal(range.read_mode, 'partial');
45
+ assert.equal(range.offset, 10);
46
+ assert.equal(range.limit, 50);
47
+ });
48
+
49
+ test('buildToolFileReadPayload flags large_file_full_read', () => {
50
+ const tmpDir = mkdtempSync(join(tmpdir(), 'onlooker-read-chunk-'));
51
+ const filePath = join(tmpDir, 'big.txt');
52
+ const bytes = LARGE_FILE_BYTES_ON_DISK + 1;
53
+ writeFileSync(filePath, 'x'.repeat(bytes), 'utf8');
54
+
55
+ const payload = buildToolFileReadPayload({ file_path: filePath }, { content: 'x\n' });
56
+ assert.equal(payload.read_mode, 'full');
57
+ assert.equal(payload.large_file_full_read, true);
58
+ assert.equal(payload.file_bytes_on_disk, bytes);
59
+
60
+ rmSync(tmpDir, { recursive: true, force: true });
61
+ });
62
+
63
+ test('mapHookInputToCanonical maps chunked Read to partial read_mode', () => {
64
+ const hookInput = loadFixture('post-tool-use-read-chunked.json');
65
+ const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
66
+ const mapped = mapHookInputToCanonical(hookInput, {
67
+ onlookerDir: tmpDir,
68
+ plugin: 'onlooker',
69
+ });
70
+
71
+ assert.equal(mapped.valid, true);
72
+ assert.equal(mapped.event.payload.read_mode, 'partial');
73
+ assert.equal(mapped.event.payload.offset, 400);
74
+ assert.equal(mapped.event.payload.limit, 80);
35
75
  assert.equal(validate(mapped.event).valid, true);
36
76
  });
37
77