@onlooker-community/ecosystem 0.10.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 (106) 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 +37 -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/portable-lock.sh +48 -0
  73. package/scripts/lib/prompt-rules.sh +207 -0
  74. package/scripts/lib/tool-history.sh +7 -8
  75. package/scripts/lib/validate-path.sh +4 -0
  76. package/scripts/lint/check-manifests.mjs +314 -0
  77. package/scripts/lint/check-references.mjs +311 -0
  78. package/skills/list-prompt-rules/SKILL.md +15 -0
  79. package/test/bats/archivist-config-files.bats +60 -0
  80. package/test/bats/archivist-config.bats +54 -0
  81. package/test/bats/archivist-inject.bats +73 -0
  82. package/test/bats/archivist-project-key.bats +75 -0
  83. package/test/bats/archivist-storage.bats +119 -0
  84. package/test/bats/archivist-ulid.bats +36 -0
  85. package/test/bats/config.bats +10 -10
  86. package/test/bats/echo-config.bats +90 -0
  87. package/test/bats/echo-events.bats +121 -0
  88. package/test/bats/echo-project-key.bats +115 -0
  89. package/test/bats/echo-stop-hook.bats +101 -0
  90. package/test/bats/echo-ulid.bats +38 -0
  91. package/test/bats/portable-lock.bats +62 -0
  92. package/test/bats/prompt-rules.bats +269 -0
  93. package/test/bats/tribunal-aggregate.bats +77 -0
  94. package/test/bats/tribunal-config.bats +86 -0
  95. package/test/bats/tribunal-events.bats +209 -0
  96. package/test/bats/tribunal-gate.bats +95 -0
  97. package/test/bats/tribunal-jury.bats +80 -0
  98. package/test/bats/tribunal-rubric.bats +119 -0
  99. package/test/bats/tribunal-stop-hook.bats +73 -0
  100. package/test/bats/tribunal-verdict.bats +71 -0
  101. package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
  102. package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
  103. package/test/helpers/setup.bash +9 -0
  104. package/test/node/check-manifests.test.mjs +173 -0
  105. package/test/node/check-references.test.mjs +279 -0
  106. package/test/node/coverage.test.mjs +143 -0
@@ -12,7 +12,6 @@
12
12
  "name": "ecosystem",
13
13
  "source": "./",
14
14
  "description": "Fill this out",
15
- "version": "0.10.0",
16
15
  "author": {
17
16
  "name": "Onlooker Community"
18
17
  },
@@ -21,6 +20,45 @@
21
20
  "license": "MIT",
22
21
  "keywords": [],
23
22
  "tags": []
23
+ },
24
+ {
25
+ "name": "archivist",
26
+ "source": "./plugins/archivist",
27
+ "description": "Structured session memory across context truncation. Extracts decisions, dead ends, and open questions on PreCompact and reinjects the most important items at SessionStart. Requires the ecosystem plugin.",
28
+ "author": {
29
+ "name": "Onlooker Community"
30
+ },
31
+ "homepage": "https://onlooker.dev",
32
+ "repository": "https://github.com/onlooker-community/ecosystem",
33
+ "license": "MIT",
34
+ "keywords": ["memory", "compaction", "context", "session"],
35
+ "tags": ["memory", "context-engineering"]
36
+ },
37
+ {
38
+ "name": "tribunal",
39
+ "source": "./plugins/tribunal",
40
+ "description": "Multi-agent execution with LLM-as-a-Judge quality gates. An Actor performs work; a jury of typed Judges scores it against a project-overridable rubric; a Meta-Judge reviews the jury for bias; the gate decides accept, retry, or exhaust. Requires the ecosystem plugin.",
41
+ "author": {
42
+ "name": "Onlooker Community"
43
+ },
44
+ "homepage": "https://onlooker.dev",
45
+ "repository": "https://github.com/onlooker-community/ecosystem",
46
+ "license": "MIT",
47
+ "keywords": ["agents", "evaluation", "quality-gates", "multi-agent", "llm-judge"],
48
+ "tags": ["agents", "evaluation"]
49
+ },
50
+ {
51
+ "name": "echo",
52
+ "source": "./plugins/echo",
53
+ "description": "Prompt-change regression detection. When a watched agent file is modified, Echo runs a single-judge quality pass via claude -p and compares the score against a stored baseline to report improved, degraded, or neutral. Requires the ecosystem plugin.",
54
+ "author": {
55
+ "name": "Onlooker Community"
56
+ },
57
+ "homepage": "https://onlooker.dev",
58
+ "repository": "https://github.com/onlooker-community/ecosystem",
59
+ "license": "MIT",
60
+ "keywords": ["evaluation", "regression", "prompt-engineering", "testing", "quality"],
61
+ "tags": ["evaluation", "testing"]
24
62
  }
25
63
  ]
26
64
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ecosystem",
3
- "version": "0.10.0",
4
- "description": "TODO fill this out",
3
+ "version": "0.14.0",
4
+ "description": "Observability substrate for Claude Code. Provides the shared ~/.onlooker/ storage root, canonical schema-validated event emission, session and tool tracking hooks, and prompt rules. Required by all other Onlooker plugins.",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
7
7
  "url": "https://onlooker.dev"
@@ -0,0 +1,46 @@
1
+ # Copilot review guidance for the Onlooker ecosystem
2
+
3
+ This file shapes Copilot's review comments and chat suggestions. Keep it tight and prescriptive — Copilot weights every line.
4
+
5
+ ## Repository shape
6
+
7
+ This is a Claude Code plugin marketplace, not a typical npm package. The default plugin (`ecosystem`) lives at the repo root and provides the observability substrate; sibling plugins live under `plugins/<name>/` and assume `~/.onlooker/` exists. Every sibling plugin has its own `.claude-plugin/plugin.json` and may have its own `hooks/hooks.json` and `scripts/`.
8
+
9
+ When reviewing changes:
10
+
11
+ * Treat `marketplace.json` and per-plugin `plugin.json` as governed by [Claude Code's plugin schema](https://code.claude.com/docs/en/plugins-reference). `version` belongs in `plugin.json` only — never in marketplace entries. Flag any PR that puts `version` on `marketplace.json plugins[]`.
12
+ * `scripts/lint/check-manifests.mjs` and `scripts/lint/check-references.mjs` are the source of truth for what's "valid." If a PR seems to violate a manifest invariant, point at those linters.
13
+ * Hooks must never block the host session. They exit 0 even on internal error, and they log failures to `~/.onlooker/logs/hook-health.jsonl` instead of throwing.
14
+
15
+ ## Locking, concurrency, portability
16
+
17
+ * Cross-process file locking goes through `lock_acquire` / `lock_release` in `scripts/lib/portable-lock.sh`. **Do not introduce new `flock` calls** — mkdir-based mutex is the convention because the hooks run on macOS without util-linux.
18
+ * Avoid bash 4+ features (associative arrays especially). macOS ships bash 3.2 by default and the hooks need to run there.
19
+ * Hooks use `set -uo pipefail`, not `set -e`. Failing fast in a hook is worse than degrading gracefully.
20
+
21
+ ## Style + tooling
22
+
23
+ * American English everywhere — commits, comments, identifiers, docs. (`color`, `behavior`, `normalize`, `analyze`.)
24
+ * Conventional Commits with a mood emoji that reflects *this* change, not the type label. Don't mechanically pair `feat: :sparkles:` or `fix: :bug:`.
25
+ * Lint stack: `biome` (JS), `shellcheck -S error -x` (bash), `markdownlint` (md). Add new shell files to the `test:shellcheck` script.
26
+ * Tests live under `test/bats/` (shell) and `test/node/` (mjs, using `node:test`). Every new helper function deserves a bats test by name — `scripts/coverage/bash-coverage.mjs` measures this and surfaces it in the PR coverage comment.
27
+ * No new runtime deps without a paragraph in the PR explaining why a stdlib-only solution doesn't fit. The validators are deliberately dependency-free.
28
+
29
+ ## Things that almost always indicate a bug
30
+
31
+ * A hook that returns non-zero on its happy path.
32
+ * A bash function definition not at column 0 (the coverage analyzer assumes top-level only).
33
+ * A markdown skill/command/agent missing `name` and `description` frontmatter (the reference linter will fail).
34
+ * A new plugin without an entry in `marketplace.json` (or with one carrying a `version` field).
35
+ * `git config --global` calls in scripts or workflows (we never want to mutate the developer's signing/auth setup).
36
+ * Writes to `~/.onlooker/` that don't honor `$ONLOOKER_DIR` overrides (breaks bats isolation).
37
+
38
+ ## Release flow
39
+
40
+ `release-please` drives versioning. Each plugin's `plugin.json.version` is bumped from its own commit history; `marketplace.json` is *not* version-bumped. `release.yml` only publishes the root ecosystem package to npm — sibling plugins are distributed via the marketplace, not npm.
41
+
42
+ ## What to focus on in reviews
43
+
44
+ In order of importance: correctness over elegance, smallest-possible-diff over architectural rewrites, evidence of testing (especially: did the PR add a bats test, a node test, or update fixtures?), and naming hygiene (kebab-case for plugin/command/skill names; snake_case for bash functions; camelCase for JS).
45
+
46
+ When in doubt about a convention, say so explicitly rather than guessing.
@@ -0,0 +1,78 @@
1
+ name: Coverage
2
+
3
+ # Runs both coverage signals on every PR + push to main:
4
+ # - node --test --experimental-test-coverage for .mjs (real line/branch/funcs)
5
+ # - bash function-reference heuristic for scripts/lib/*.sh and plugins/*/scripts/lib/*.sh
6
+ #
7
+ # On pull requests, posts a single markdown comment that updates in place
8
+ # (keyed by the `<!-- onlooker-coverage-comment -->` sentinel) so the PR
9
+ # discussion isn't spammed with one comment per push.
10
+
11
+ on:
12
+ pull_request:
13
+ push:
14
+ branches:
15
+ - main
16
+
17
+ permissions:
18
+ contents: read
19
+ pull-requests: write
20
+
21
+ jobs:
22
+ coverage:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - name: Install mise
28
+ uses: jdx/mise-action@v2
29
+
30
+ - name: Install tools
31
+ run: mise install
32
+
33
+ - name: Install Node dependencies
34
+ run: npm ci
35
+
36
+ - name: Run node coverage
37
+ run: node scripts/coverage/run-coverage.mjs --json > coverage-node.json
38
+
39
+ - name: Run bash function coverage
40
+ run: node scripts/coverage/bash-coverage.mjs --json > coverage-bash.json
41
+
42
+ - name: Render markdown comment
43
+ run: |
44
+ node scripts/coverage/format-comment.mjs \
45
+ --node coverage-node.json \
46
+ --bash coverage-bash.json \
47
+ --sha "$GITHUB_SHA" > coverage-comment.md
48
+ cat coverage-comment.md
49
+
50
+ - name: Upload coverage artifacts
51
+ uses: actions/upload-artifact@v4
52
+ with:
53
+ name: coverage-${{ github.run_id }}
54
+ path: |
55
+ coverage-node.json
56
+ coverage-bash.json
57
+ coverage-comment.md
58
+
59
+ - name: Upsert PR comment
60
+ if: github.event_name == 'pull_request'
61
+ env:
62
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63
+ PR_NUMBER: ${{ github.event.pull_request.number }}
64
+ run: |
65
+ # Find any existing coverage comment by the sentinel, edit it if
66
+ # found, otherwise create a fresh one. Single comment per PR.
67
+ existing=$(gh pr view "$PR_NUMBER" --json comments \
68
+ --jq '.comments[] | select(.body | startswith("<!-- onlooker-coverage-comment -->")) | .url' \
69
+ | head -n1)
70
+ if [[ -n "$existing" ]]; then
71
+ comment_id="${existing##*#issuecomment-}"
72
+ gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" \
73
+ -f body="$(cat coverage-comment.md)" >/dev/null
74
+ echo "Updated comment ${comment_id}"
75
+ else
76
+ gh pr comment "$PR_NUMBER" --body-file coverage-comment.md
77
+ echo "Posted new comment"
78
+ fi
@@ -12,24 +12,40 @@ jobs:
12
12
  release-please:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
+ # Force release-please's intermediate PR-branch commits to be authored
16
+ # by github-actions[bot] instead of inheriting whatever identity the
17
+ # PAT owner happens to have configured on their local machine. The
18
+ # squash-merge of the release PR already attributes the merge to the
19
+ # PAT owner correctly, but the intermediate commits would otherwise
20
+ # leak the maintainer's hostname-shaped author email.
21
+ - name: Configure bot identity
22
+ run: |
23
+ git config --global user.name 'github-actions[bot]'
24
+ git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
25
+
15
26
  - uses: googleapis/release-please-action@v4
16
27
  id: release
17
28
  with:
18
29
  token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
19
-
30
+
31
+ # Only publish to npm when the ecosystem (root) package was released.
32
+ # Archivist (and future sibling plugins) are distributed via the Claude
33
+ # Code marketplace, not npm — a release for a sibling alone must not
34
+ # trigger `npm publish`, which would attempt to re-publish ecosystem at
35
+ # its existing version and fail.
20
36
  - uses: actions/checkout@v4
21
- if: ${{ steps.release.outputs.release_created }}
22
-
37
+ if: ${{ steps.release.outputs['.--release_created'] }}
38
+
23
39
  - uses: actions/setup-node@v4
24
- if: ${{ steps.release.outputs.release_created }}
40
+ if: ${{ steps.release.outputs['.--release_created'] }}
25
41
  with:
26
42
  node-version: '22'
27
43
  registry-url: 'https://registry.npmjs.org'
28
-
44
+
29
45
  - run: npm ci
30
- if: ${{ steps.release.outputs.release_created }}
31
-
46
+ if: ${{ steps.release.outputs['.--release_created'] }}
47
+
32
48
  - run: npm publish
33
- if: ${{ steps.release.outputs.release_created }}
49
+ if: ${{ steps.release.outputs['.--release_created'] }}
34
50
  env:
35
51
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -6,6 +6,9 @@ on:
6
6
  branches:
7
7
  - main
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  jobs:
10
13
  test:
11
14
  name: Test
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ # release-please generated; regenerated on every release, not authored
3
+ **/CHANGELOG.md
@@ -1,3 +1,6 @@
1
1
  {
2
- ".": "0.10.0"
2
+ ".": "0.14.0",
3
+ "plugins/archivist": "0.1.0",
4
+ "plugins/tribunal": "1.0.0",
5
+ "plugins/echo": "0.2.0"
3
6
  }
package/CHANGELOG.md CHANGED
@@ -7,6 +7,43 @@
7
7
 
8
8
  # Changelog
9
9
 
10
+ ## [0.14.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.13.0...ecosystem-v0.14.0) (2026-05-25)
11
+
12
+
13
+ ### Features
14
+
15
+ * **echo:** add prompt regression detection plugin ([#32](https://github.com/onlooker-community/ecosystem/issues/32)) ([65274d4](https://github.com/onlooker-community/ecosystem/commit/65274d4d8326950d6c998ca292fed13b1b8c493b))
16
+
17
+ ## [0.13.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.12.0...ecosystem-v0.13.0) (2026-05-24)
18
+
19
+
20
+ ### Features
21
+
22
+ * **tribunal:** add multi-agent code review plugin :sparkles: ([#30](https://github.com/onlooker-community/ecosystem/issues/30)) ([893f24a](https://github.com/onlooker-community/ecosystem/commit/893f24a8876fdd6ccb5c7dcf2636a7c902e88949))
23
+
24
+ ## [0.12.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.11.0...ecosystem-v0.12.0) (2026-05-23)
25
+
26
+
27
+ ### Features
28
+
29
+ * **prompt-rules:** deterministic regex-triggered guidance injection :relieved: ([#28](https://github.com/onlooker-community/ecosystem/issues/28)) ([662c811](https://github.com/onlooker-community/ecosystem/commit/662c8119657cebc350900f859c43dbaca97d6703))
30
+
31
+ ## [0.11.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.10.0...ecosystem-v0.11.0) (2026-05-23)
32
+
33
+
34
+ ### Features
35
+
36
+ * **archivist:** introduce structured session memory plugin :rocket: ([378fff3](https://github.com/onlooker-community/ecosystem/commit/378fff3c14b40644af45b1a2335992e7b0428160))
37
+ * **coverage:** report node + bash coverage on every PR :sparkles: ([cb5d122](https://github.com/onlooker-community/ecosystem/commit/cb5d1221ad20e6257d66b507897dae14549a870f))
38
+ * **lint:** add marketplace cross-reference linter :nail_care: ([0f48817](https://github.com/onlooker-community/ecosystem/commit/0f488170326659ef1d0b8bd7ae4d207c78a43694))
39
+ * **lint:** add plugin manifest validator :nail_care: ([e12615f](https://github.com/onlooker-community/ecosystem/commit/e12615ff99d43caf59d5e215d882c0acb3352c01))
40
+
41
+
42
+ ### Bug Fixes
43
+
44
+ * **hooks:** replace flock with portable mkdir mutex :bug: ([3dffa6f](https://github.com/onlooker-community/ecosystem/commit/3dffa6f5e43ef9f3c117f2406ddd03ce485df1cd))
45
+ * **release:** sync marketplace.json out-of-band :relieved: ([0d2a0a3](https://github.com/onlooker-community/ecosystem/commit/0d2a0a38c0c4ee0e400b9a143a4be7904ea3f70a))
46
+
10
47
  ## [0.10.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.9.0...ecosystem-v0.10.0) (2026-05-22)
11
48
 
12
49
 
package/README.md CHANGED
@@ -1,33 +1,77 @@
1
1
  # Onlooker Ecosystem
2
2
 
3
+ [![Test](https://github.com/onlooker-community/ecosystem/actions/workflows/test.yml/badge.svg)](https://github.com/onlooker-community/ecosystem/actions/workflows/test.yml)
4
+
3
5
  Agents, skills, hooks, commands, rules, and MCP configurations that power [Onlooker](https://onlooker.dev).
4
6
 
7
+ The ecosystem is a **Claude Code plugin marketplace** built around a shared observability substrate. Every plugin writes to a common event log, derives stable project keys from your git remote, and stores artifacts under `~/.onlooker/` — so plugins compose without stepping on each other, and every event is queryable in one place.
8
+
9
+ ---
10
+
11
+ ## Plugins
12
+
13
+ | Plugin | Description | Opt-in? |
14
+ |--------|-------------|---------|
15
+ | [`ecosystem`](./) | Observability substrate: session tracking, canonical events, prompt rules, tool history. Required by all other plugins. | No — always on |
16
+ | [`archivist`](./plugins/archivist) | Structured session memory across context truncation. Extracts decisions, dead ends, and open questions; reinjects the most relevant items at the start of the next session. | Yes — disabled by default |
17
+ | [`tribunal`](./plugins/tribunal) | Multi-agent quality gates. Wraps a task in an Actor → Jury → Meta-Judge → Gate loop; retries the Actor with critique until the gate passes or `max_iterations` is reached. | Yes — skill always available; Stop hook opt-in |
18
+ | [`echo`](./plugins/echo) | Prompt-change regression detection. When a watched agent file is modified, runs a quality pass and reports whether the change improved, degraded, or had no measurable effect. | Yes — disabled by default |
19
+
20
+ For how these fit together, see [docs/architecture.md](docs/architecture.md).
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ ```bash
27
+ # Install the Onlooker CLI
28
+ brew tap onlooker-community/tap
29
+ brew install onlooker
30
+
31
+ # Run the guided setup wizard
32
+ onlooker setup
33
+ ```
34
+
5
35
  ---
6
36
 
7
37
  ## Development
8
38
 
9
- Install tools with [mise](https://mise.jdx.dev/) (`mise install`), then install dependencies (includes [`@onlooker-community/schema`](https://www.npmjs.com/package/@onlooker-community/schema) from npm):
39
+ Install tools with [mise](https://mise.jdx.dev/) (`mise install`), then install dependencies:
10
40
 
11
41
  ```bash
12
42
  npm ci
13
- npm test # bats + schema validation tests
43
+ npm test # bats + schema validation tests
14
44
  npm run test:shellcheck
15
- npm run test:ci # shellcheck + bats + schema + lint
45
+ npm run test:ci # shellcheck + bats + schema + lint
16
46
  ```
17
47
 
18
- Hooks emit [canonical Onlooker events](https://github.com/onlooker-community/schema) via `scripts/lib/onlooker-event.mjs`. Bash helpers live in `scripts/lib/onlooker-schema.sh`.
48
+ Hooks emit [canonical Onlooker events](https://github.com/onlooker-community/schema) via `scripts/lib/onlooker-event.mjs`. Bash helpers live in `scripts/lib/`.
19
49
 
20
- Tests live under `test/bats/` and `test/node/` and use an isolated temp home so nothing writes to your real `~/.onlooker`.
50
+ Tests live under `test/bats/` and `test/node/` and use an isolated temp home so nothing writes to your real `~/.onlooker/`.
21
51
 
22
- ## Quick Start
52
+ ---
23
53
 
24
- Get up and running in under 2 minutes:
54
+ ## Prompt rules
25
55
 
26
- ```bash
27
- # Use homebrew to install Onlooker client
28
- brew tap onlooker-community/tap
29
- brew install onlooker
56
+ The ecosystem plugin ships a `UserPromptSubmit` hook that injects declarative guidance when a user prompt matches a regex. Rules fire deterministically on literal prompt-text patterns, filling the niche that skills can't: guidance that must fire regardless of whether the model would have chosen a skill.
30
57
 
31
- # Use the wizard to be guided through setup
32
- onlooker setup
58
+ Rules live in two files:
59
+
60
+ - `~/.onlooker/prompt-rules.json` — global, applies across all projects
61
+ - `<repo>/.claude/prompt-rules.json` — project-level, overrides global by `id`
62
+
63
+ ```json
64
+ {
65
+ "rules": [
66
+ {
67
+ "id": "rule-no-verify-warning",
68
+ "pattern": "--no-verify",
69
+ "guidance": "Skipping hooks usually masks the real issue. Investigate the failure first.",
70
+ "fire_once_per_session": true,
71
+ "enabled": true
72
+ }
73
+ ]
74
+ }
33
75
  ```
76
+
77
+ `pattern` is POSIX ERE (`[[ =~ ]]` semantics). Run `/list-prompt-rules` to see active rules and per-session fire state.
package/config.json CHANGED
@@ -1,4 +1,9 @@
1
1
  {
2
2
  "plugin_name": "onlooker",
3
- "storage_path": "~/.onlooker"
3
+ "storage_path": "~/.onlooker",
4
+ "prompt_rules": {
5
+ "enabled": true,
6
+ "per_turn_max_chars": 1200,
7
+ "max_rules": 50
8
+ }
4
9
  }
@@ -0,0 +1,43 @@
1
+ # ADR-001: Claude Code Hooks as the Integration Surface
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-05-24
5
+
6
+ ## Context
7
+
8
+ Onlooker needs to observe what happens inside a Claude Code session — when sessions start and end, what tools are called, when context compacts, and when the model produces output. Several integration approaches were available:
9
+
10
+ - **Claude Code hooks** — shell commands registered in `settings.json` that fire on lifecycle events (`SessionStart`, `Stop`, `PreCompact`, `PostToolUse`, `UserPromptSubmit`).
11
+ - **MCP server** — a Model Context Protocol server that Claude Code connects to; the server receives tool calls and can inject context.
12
+ - **Wrapper CLI** — a `claude` shim that intercepts invocations, records them, and delegates to the real binary.
13
+ - **IDE extension** — a VS Code or JetBrains extension that observes editor events.
14
+
15
+ ## Decision
16
+
17
+ The ecosystem uses **Claude Code hooks** as the primary integration surface.
18
+
19
+ ## Rationale
20
+
21
+ **First-class support.** Hooks are a documented, stable feature of Claude Code. They receive structured JSON on stdin and can inject `additionalContext` via stdout. This is not a workaround — it's the intended extension mechanism.
22
+
23
+ **No daemon required.** Hooks are short-lived shell processes. No persistent server, no port management, no process supervision. Each hook fires, does its work, and exits. This keeps the operational footprint near zero.
24
+
25
+ **Composable and auditable.** Hook registrations live in `settings.json` alongside permissions and tool config. A developer can inspect exactly which hooks are registered, enable or disable them per-project, and trace any hook's behavior by reading its shell script.
26
+
27
+ **Works across all Claude Code surfaces.** Hooks fire in the CLI, the desktop app, and IDE extensions. An MCP server or wrapper CLI would cover only the surfaces that honor those integrations.
28
+
29
+ **Shell is universally available.** Hooks are shell commands. The Onlooker substrate (bash, jq, node) is the only runtime requirement. An MCP server would require a language runtime and a persistent process to be managed.
30
+
31
+ ## Why not MCP?
32
+
33
+ MCP is the right surface for extending Claude's *tool use* — adding new capabilities the model can call. It is not a good fit for *observing* what the model does. MCP servers cannot easily intercept session lifecycle events (start, stop, compact), and the connection model assumes a persistent server. Hooks handle lifecycle events natively and are stateless by design.
34
+
35
+ ## Why not a wrapper CLI?
36
+
37
+ A `claude` shim breaks when users install Claude Code via paths the shim does not intercept (desktop app, IDE extension). It also requires the shim to be on PATH before the real binary, which is fragile across environments. Hooks require no PATH manipulation.
38
+
39
+ ## Consequences
40
+
41
+ - All ecosystem behavior is implemented in shell scripts sourced from `scripts/hooks/` and `scripts/lib/`. Shell has limitations (no associative arrays on bash 3.2, string-only data model), but the constraints are well-understood and the code is readable without a language-specific toolchain.
42
+ - The hook event set is fixed by Claude Code. If an event that doesn't exist today is needed (e.g., a `PostCompact` or `ToolError` event), it cannot be added until Claude Code ships it.
43
+ - Hooks run synchronously in some cases (`UserPromptSubmit`, `Stop`) and must be fast or the user experience degrades. Long-running evaluations (Tribunal, Echo) use `claude -p` subprocesses and must carry a recursion guard.
@@ -0,0 +1,39 @@
1
+ # ADR-002: Centralized JSONL Event Log with Schema Validation
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-05-24
5
+
6
+ ## Context
7
+
8
+ Every plugin produces structured signals — session timings, Tribunal verdicts, Echo drift scores, Archivist extraction events. These signals need to be stored somewhere. Options considered:
9
+
10
+ - **Per-plugin flat files** — each plugin writes its own log in its own format.
11
+ - **SQLite database** — a structured store under `~/.onlooker/`.
12
+ - **Centralized JSONL log** — all plugins append to a single `~/.onlooker/logs/onlooker-events.jsonl` file, with a schema-validated envelope per event.
13
+ - **Remote backend only** — events are sent directly to a cloud endpoint; nothing stored locally.
14
+
15
+ ## Decision
16
+
17
+ All schema-defined events are written to a **centralized JSONL log** at `~/.onlooker/logs/onlooker-events.jsonl`, validated against [`@onlooker-community/schema`](https://github.com/onlooker-community/schema) before write. The log may also contain non-schema events from hooks that predate or have not yet been ported to the canonical pipeline (see Consequences).
18
+
19
+ ## Rationale
20
+
21
+ **One place to query.** A single log means a dashboard, script, or downstream consumer can read everything without knowing which plugins are installed or where each one writes. Cross-plugin queries (e.g., "show Tribunal verdicts alongside the Echo drift scores for the same session") require no joins across separate stores.
22
+
23
+ **JSONL is the simplest durable format.** One JSON object per line. Human-readable with `jq`. Appendable without locking (each `printf '%s\n'` is atomic on POSIX filesystems for lines under the page size). No schema migrations, no vacuum, no connection management.
24
+
25
+ **Schema validation at write time prevents silent corruption.** Each event is validated before it is appended. If a plugin emits a malformed payload, the write fails loudly — the hook logs an error and the line is never added to the log. This means the log is always a valid, queryable dataset. Per-plugin flat files with ad-hoc formats would accumulate inconsistencies silently.
26
+
27
+ **Versioned schema as a contract.** The schema is published as `@onlooker-community/schema` on npm and versioned independently. Plugins declare which schema version they target. When the schema adds new event types (e.g., the `echo.*` events added in v2.2.0), old plugins continue to work — they just don't emit the new types. Consumers can use the schema version field to handle evolution.
28
+
29
+ **Local-first for privacy.** All data stays on the developer's machine by default. A future cloud-sync feature can read the JSONL and upload selectively; the log itself makes no network calls.
30
+
31
+ ## Why not SQLite?
32
+
33
+ SQLite would give us structured queries, indexes, and transactions. The tradeoff is operational complexity: the file has a binary format (not inspectable with `cat`/`jq`), requires a SQLite binary, and has locking behavior that could deadlock if multiple hook processes fire concurrently. JSONL with append-only writes has no locking issue and is trivially inspectable.
34
+
35
+ ## Consequences
36
+
37
+ - The log grows indefinitely. A future rotation/archival feature is needed for long-lived developer machines. Currently operators must prune manually.
38
+ - The goal is that all event types are defined in `@onlooker-community/schema` before a plugin emits them. Adding a new event type requires a schema release — intentional friction that prevents undocumented shapes from accumulating. In practice, `prompt_rule.*` events are a current exception: they are emitted to the log by the prompt-rules hook but are not yet defined in the schema. This should be resolved in a future schema release.
39
+ - Concurrent appends from multiple hooks (e.g., a `PostToolUse` hook and a `Stop` hook firing close together) are safe for lines under ~4 KB on POSIX, but multi-kilobyte payloads could interleave. In practice, event payloads are small and this has not been an issue.
@@ -0,0 +1,40 @@
1
+ # ADR-003: ULID for All Identifiers
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-05-24
5
+
6
+ ## Context
7
+
8
+ Every artifact in the ecosystem — Archivist memories, Tribunal tasks and iterations, Echo suites and tests — needs a unique identifier. The identifier is used as a filename, a log correlation key, and a sort key. Options considered:
9
+
10
+ - **UUID v4** — random, universally supported, 36 chars with hyphens.
11
+ - **UUID v7** — time-ordered UUID, requires a library on older runtimes.
12
+ - **ULID** — Universally Unique Lexicographically Sortable Identifier. 26 chars, Crockford Base32, time-ordered to millisecond precision.
13
+ - **Timestamp + random suffix** — ad-hoc, human-readable but not globally unique.
14
+ - **Sequential integer** — simple but requires a counter store and fails across processes.
15
+
16
+ ## Decision
17
+
18
+ All ecosystem identifiers use **ULID**.
19
+
20
+ ## Rationale
21
+
22
+ **Lexicographically sortable = chronologically sortable.** ULIDs sort correctly with `ls`, `sort`, and JSONL readers without parsing a timestamp field. Artifact directories and log entries naturally order by creation time. With UUID v4, sorting by filename requires a separate `created_at` index.
23
+
24
+ **No hyphens, no special characters.** ULIDs use Crockford Base32 (characters `0-9A-Z` excluding `I`, `L`, `O`, `U`). They are safe in filenames, URLs, and environment variables without quoting or encoding. UUID hyphens require quoting in some shell contexts.
25
+
26
+ **Compact.** 26 characters vs. 36 for UUID. Minor, but it matters in log lines and filenames that appear in terminal output.
27
+
28
+ **Millisecond time prefix enables time-range queries.** The first 10 characters of a ULID encode the Unix timestamp in milliseconds. A script can filter the JSONL log to a time range by string-comparing ULID prefixes without parsing every `timestamp` field.
29
+
30
+ **No runtime dependency.** Each plugin ships its own `echo-ulid.sh` / `archivist-ulid.sh` / `tribunal-ulid.sh` generator implemented in pure bash (with a `python3` fallback for millisecond timestamps on macOS, where `date +%s%3N` is broken). No UUID library needed.
31
+
32
+ ## Why not UUID v7?
33
+
34
+ UUID v7 is time-ordered like ULID but uses the standard UUID format (32 hex + 4 hyphens). The tradeoffs vs. ULID are: more characters, hyphen special-casing in filenames, and no widely available pure-bash generator. ULID is the better fit for a shell-first ecosystem.
35
+
36
+ ## Consequences
37
+
38
+ - Every plugin that generates IDs ships its own `*-ulid.sh` library. This is intentional duplication to avoid cross-plugin runtime dependencies, but it means ULID generation logic exists in three places. All three are tested identically via bats.
39
+ - ULID has 80 bits of randomness in the lower 80 bits (after the 48-bit timestamp). The collision probability is negligible for the volumes this ecosystem handles, but not zero.
40
+ - The time-ordered property is only guaranteed within a single millisecond. ULIDs generated in the same millisecond are random in the lower bits, so their relative order is not deterministic. This is acceptable — within a session, events are ordered by emission, not by ULID sort.
@@ -0,0 +1,34 @@
1
+ # ADR-004: Per-Plugin Config with settings.json Overlay
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-05-24
5
+
6
+ ## Context
7
+
8
+ Plugins need to be configurable. A developer working on a security-sensitive repo wants a tighter Tribunal gate policy; a developer on a fast-iteration project wants Echo to use a cheaper model. Several config models were available:
9
+
10
+ - **Single global config** — one file under `~/.onlooker/` controls everything.
11
+ - **Plugin-owned config only** — each plugin reads its own file; no user override path.
12
+ - **Separate per-project config files** — e.g., `.onlooker/echo.json`, `.onlooker/tribunal.json`.
13
+ - **Plugin defaults + settings.json overlay** — plugin ships `config.json` with defaults; users override via the standard Claude Code `settings.json` under a plugin-namespaced key.
14
+
15
+ ## Decision
16
+
17
+ Each plugin ships `config.json` with defaults. Users override per-project in `.claude/settings.json` (or globally in `~/.claude/settings.json`) under the plugin's namespace key (e.g., `"echo"`, `"tribunal"`).
18
+
19
+ ## Rationale
20
+
21
+ **`settings.json` is already the Claude Code config file.** Developers already open `.claude/settings.json` to configure permissions, hooks, and tools. Putting plugin config in the same file means one file to edit, one file to commit, one file to review in a PR. Introducing `.onlooker/echo.json` as a separate file creates fragmentation without benefit.
22
+
23
+ **Two levels cover the common cases without complexity.** Global (`~/.claude/settings.json`) sets your personal defaults — the model you prefer, your default drift threshold. Project-level (`.claude/settings.json`) overrides for the specific repo. Most plugin config systems need exactly these two scopes; adding more (team, org, workspace) introduces merging ambiguity.
24
+
25
+ **Plugin `config.json` is the source of truth for available knobs.** Every configurable key is documented in the plugin's `config.json` with its default value. Users browse the defaults to discover what's overridable. This is simpler than a separate schema document.
26
+
27
+ **Config loading is a thin bash function.** Each plugin ships a `*-config.sh` library (`echo_config_get`, `tribunal_config_get`, etc.) that reads the settings overlay first, falls back to plugin defaults. The pattern is consistent across plugins and is tested via bats.
28
+
29
+ ## Consequences
30
+
31
+ - Plugin config keys must not collide with existing Claude Code top-level keys (`permissions`, `hooks`, `mcpServers`, `env`, etc.). Plugin namespaces (`"echo"`, `"tribunal"`, `"archivist"`) are chosen to avoid conflicts.
32
+ - Merge behavior is not yet uniform across plugins. Tribunal and Archivist use a recursive `deepmerge` (implemented in jq) so a user can override a single nested key without replacing the whole sub-object. Echo uses a simpler per-key lookup that falls back to plugin defaults. New plugins should use `deepmerge` for consistency.
33
+ - `config.json` is committed to the repo and ships with the plugin. It is not user-editable in place — users always override via `settings.json`. This prevents accidental plugin updates from overwriting user config.
34
+ - There is no validation that `settings.json` keys are recognized by the plugin. An unknown key silently does nothing. A future lint step could warn on unrecognized plugin config keys.