@onlooker-community/ecosystem 0.18.0 → 0.20.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.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +4 -2
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +1 -0
- package/docs/memory-architecture.md +102 -0
- package/package.json +3 -3
- package/plugins/curator/docs/adr/001-staleness-tiers.md +100 -0
- package/plugins/curator/docs/design.md +311 -0
- package/plugins/historian/docs/adr/001-local-embeddings-only.md +96 -0
- package/plugins/historian/docs/design.md +317 -0
- package/plugins/librarian/.claude-plugin/plugin.json +14 -0
- package/plugins/librarian/CHANGELOG.md +10 -0
- package/plugins/librarian/README.md +51 -0
- package/plugins/librarian/config.json +52 -0
- package/plugins/librarian/docs/adr/001-propose-dont-auto-write.md +87 -0
- package/plugins/librarian/docs/design.md +301 -0
- package/plugins/librarian/hooks/hooks.json +26 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +312 -0
- package/plugins/librarian/scripts/hooks/librarian-session-start.sh +103 -0
- package/plugins/librarian/scripts/lib/librarian-archivist-reader.sh +67 -0
- package/plugins/librarian/scripts/lib/librarian-classifier.sh +139 -0
- package/plugins/librarian/scripts/lib/librarian-config.sh +74 -0
- package/plugins/librarian/scripts/lib/librarian-durability.sh +77 -0
- package/plugins/librarian/scripts/lib/librarian-emit.sh +72 -0
- package/plugins/librarian/scripts/lib/librarian-project-key.sh +83 -0
- package/plugins/librarian/scripts/lib/librarian-storage.sh +222 -0
- package/plugins/librarian/scripts/lib/librarian-ulid.sh +50 -0
- package/plugins/warden/.claude-plugin/plugin.json +14 -0
- package/plugins/warden/CHANGELOG.md +10 -0
- package/plugins/warden/config.json +51 -0
- package/plugins/warden/docs/adr/001-detect-after-ingest-gate-before-action.md +62 -0
- package/plugins/warden/docs/design.md +123 -0
- package/plugins/warden/hooks/hooks.json +73 -0
- package/plugins/warden/scripts/hooks/warden-post-tool-use.sh +201 -0
- package/plugins/warden/scripts/hooks/warden-pre-tool-use.sh +94 -0
- package/plugins/warden/scripts/hooks/warden-session-start.sh +52 -0
- package/plugins/warden/scripts/lib/warden-cli.sh +124 -0
- package/plugins/warden/scripts/lib/warden-config.sh +79 -0
- package/plugins/warden/scripts/lib/warden-evaluator.sh +246 -0
- package/plugins/warden/scripts/lib/warden-events.sh +85 -0
- package/plugins/warden/scripts/lib/warden-gate-state.sh +105 -0
- package/plugins/warden/scripts/lib/warden-patterns.sh +132 -0
- package/plugins/warden/scripts/lib/warden-sanitizer.sh +80 -0
- package/plugins/warden/scripts/lib/warden-scanner.sh +119 -0
- package/plugins/warden/scripts/lib/warden-ulid.sh +50 -0
- package/plugins/warden/skills/warden/SKILL.md +49 -0
- package/release-please-config.json +32 -0
- package/test/bats/librarian-session-end.bats +182 -0
- package/test/bats/librarian-session-start.bats +136 -0
- package/test/bats/warden-config.bats +54 -0
- package/test/bats/warden-events.bats +85 -0
- package/test/bats/warden-gate-state.bats +67 -0
- package/test/bats/warden-patterns.bats +58 -0
- package/test/bats/warden-sanitizer.bats +53 -0
- package/test/bats/warden-scanner.bats +56 -0
- package/test/bats/warden-ulid.bats +30 -0
|
@@ -111,6 +111,19 @@
|
|
|
111
111
|
"license": "MIT",
|
|
112
112
|
"keywords": ["synthesis", "recommendations", "observability", "coaching", "patterns", "weekly"],
|
|
113
113
|
"tags": ["observability", "coaching"]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "warden",
|
|
117
|
+
"source": "./plugins/warden",
|
|
118
|
+
"description": "Untrusted-content gate. Scans content flowing in through WebFetch and Read for prompt-injection patterns, and when a threat is detected closes a session-scoped gate that blocks Write, Edit, and Bash until the user explicitly clears it. Grounded in Meta's Agents Rule of Two — warden removes the agent's external-actions property while untrusted content is in play. Requires the ecosystem plugin.",
|
|
119
|
+
"author": {
|
|
120
|
+
"name": "Onlooker Community"
|
|
121
|
+
},
|
|
122
|
+
"homepage": "https://onlooker.dev",
|
|
123
|
+
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
124
|
+
"license": "MIT",
|
|
125
|
+
"keywords": ["security", "prompt-injection", "rule-of-two", "safety", "content-gate", "untrusted-content"],
|
|
126
|
+
"tags": ["safety", "security"]
|
|
114
127
|
}
|
|
115
128
|
]
|
|
116
129
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
".": "0.
|
|
2
|
+
".": "0.20.0",
|
|
3
3
|
"plugins/archivist": "0.1.0",
|
|
4
4
|
"plugins/tribunal": "1.0.1",
|
|
5
5
|
"plugins/echo": "0.2.0",
|
|
@@ -7,5 +7,7 @@
|
|
|
7
7
|
"plugins/governor": "0.2.0",
|
|
8
8
|
"plugins/compass": "0.2.0",
|
|
9
9
|
"plugins/scribe": "0.2.0",
|
|
10
|
-
"plugins/counsel": "0.2.0"
|
|
10
|
+
"plugins/counsel": "0.2.0",
|
|
11
|
+
"plugins/warden": "0.2.0",
|
|
12
|
+
"plugins/librarian": "0.1.0"
|
|
11
13
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.20.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.19.0...ecosystem-v0.20.0) (2026-06-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **librarian:** land plugin end-to-end with memory layer designs :seedling: ([#55](https://github.com/onlooker-community/ecosystem/issues/55)) ([d4821ef](https://github.com/onlooker-community/ecosystem/commit/d4821efabfeb587e460e898d7db8f92fcc3f2c61))
|
|
9
|
+
|
|
10
|
+
## [0.19.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.18.0...ecosystem-v0.19.0) (2026-06-02)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **warden:** untrusted-content gate enforcing the Agents Rule of Two :shield: ([#53](https://github.com/onlooker-community/ecosystem/issues/53)) ([210aa51](https://github.com/onlooker-community/ecosystem/commit/210aa51bff66226a0eec1f17292a2af4ea4ef56a))
|
|
16
|
+
|
|
3
17
|
## [0.18.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.17.0...ecosystem-v0.18.0) (2026-06-02)
|
|
4
18
|
|
|
5
19
|
|
package/CLAUDE.md
CHANGED
|
@@ -36,6 +36,7 @@ scripts/lib/onlooker-event.mjs ← canonical event builder; all plugins route t
|
|
|
36
36
|
| echo | Stop | Regression-tests prompt changes after each agent stop |
|
|
37
37
|
| governor | SessionStart, PreToolUse (Task), PostToolUse (Task), Stop | Budget gates on subagent spawns; tracks spend per session |
|
|
38
38
|
| tribunal | Stop + skill invocation | Post-task quality gate; also invokable via `/tribunal` |
|
|
39
|
+
| warden | PostToolUse (WebFetch, Read), PreToolUse (Write, Edit, MultiEdit, Bash), SessionStart + skill invocation | Scans ingested content for injection; closes a content gate that blocks write-class tools until cleared via `/warden` |
|
|
39
40
|
|
|
40
41
|
Plugins communicate by emitting events to the JSONL log — they do not call each other directly. All plugins depend on the ecosystem substrate; no plugin depends on another plugin directly.
|
|
41
42
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Memory Architecture
|
|
2
|
+
|
|
3
|
+
This document describes how the Onlooker ecosystem manages memory across timescales — from the in-session working context, through compaction-resistant session memory, into durable cross-session knowledge, and back out as just-in-time recall.
|
|
4
|
+
|
|
5
|
+
It is the companion to [`architecture.md`](architecture.md), which describes the substrate. This doc describes how the memory-flavored plugins compose on top of it.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The four timescales
|
|
10
|
+
|
|
11
|
+
| Timescale | Lifetime | Substrate | Plugin |
|
|
12
|
+
|-----------|----------|-----------|--------|
|
|
13
|
+
| Working | Within one model turn | Conversation context window | (the model itself) |
|
|
14
|
+
| Session | One Claude Code session, across compactions | `~/.onlooker/archivist/<project-key>/` | **archivist** |
|
|
15
|
+
| Durable | Across sessions, weeks–months | `~/.claude/projects/<encoded-project>/memory/` | **librarian**, **curator** |
|
|
16
|
+
| Episodic | Across sessions, retrieved on similarity | `~/.onlooker/historian/<project-key>/` | **historian** |
|
|
17
|
+
|
|
18
|
+
Each plugin operates on exactly one substrate. They communicate by events, not by reaching across substrates.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## The pipeline
|
|
23
|
+
|
|
24
|
+
```mermaid
|
|
25
|
+
flowchart LR
|
|
26
|
+
session[Active session]
|
|
27
|
+
session -->|PreCompact| archivist[(archivist artifacts<br/>per session)]
|
|
28
|
+
archivist -->|SessionEnd| librarian{librarian:<br/>worth keeping?}
|
|
29
|
+
librarian -->|propose| memstore[(typed memory store<br/>user / feedback / project / reference)]
|
|
30
|
+
memstore -->|SessionStart audit| curator{curator:<br/>still valid?}
|
|
31
|
+
curator -->|prune proposals| memstore
|
|
32
|
+
|
|
33
|
+
session -->|SessionEnd| historian[(historian vector store<br/>transcript embeddings)]
|
|
34
|
+
historian -->|UserPromptSubmit similarity| session
|
|
35
|
+
memstore -->|SessionStart inject| session
|
|
36
|
+
archivist -->|SessionStart inject| session
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Flow of a fact
|
|
40
|
+
|
|
41
|
+
A non-obvious project fact ("the auth migration is driven by legal, not tech debt") goes through:
|
|
42
|
+
|
|
43
|
+
1. **Captured during session** — surfaces in the conversation, lands in the transcript.
|
|
44
|
+
2. **Extracted at compaction by archivist** — written as a `decisions/<ulid>.json` artifact when context fills.
|
|
45
|
+
3. **Reinjected by archivist next session** — appears in `additionalContext` if it ranks within the recency/pinning budget. The fact survives session boundaries but lives in a per-session, recency-ranked space.
|
|
46
|
+
4. **Promoted by librarian** — at session end (or on demand), librarian decides "this should be a `project` memory" and writes it to the typed memory store with provenance pointing back to the archivist artifact.
|
|
47
|
+
5. **Audited by curator** — periodically, curator checks whether the **Why:** is still load-bearing (has the legal review concluded? did the migration land?). If stale, curator surfaces a prune proposal.
|
|
48
|
+
6. **Recalled by historian (separately)** — if a future session encounters a similar problem ("we're being asked to rip out X middleware again"), historian retrieves the transcript chunk from the original session where this was discussed, providing fuller context than the distilled project memory can carry.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Boundaries between memory plugins
|
|
53
|
+
|
|
54
|
+
The memory plugins overlap in spirit but operate on different substrates and answer different questions. Misunderstanding these boundaries is the most likely failure mode.
|
|
55
|
+
|
|
56
|
+
| Plugin | Question it answers | Storage | Trigger |
|
|
57
|
+
|--------|---------------------|---------|---------|
|
|
58
|
+
| archivist | "What did we decide / try / leave open in *this* session?" | `~/.onlooker/archivist/<project-key>/` | PreCompact, SessionStart |
|
|
59
|
+
| librarian | "Which of those decisions deserves to live forever?" | `~/.claude/projects/<encoded-project>/memory/` (writes proposals; user confirms) | SessionEnd, scheduled |
|
|
60
|
+
| curator | "Which of our durable memories are now wrong, stale, or contradictory?" | reads & proposes prunes against the typed memory store | SessionStart (cheap), scheduled (LLM) |
|
|
61
|
+
| historian | "Have we seen something like this before, and if so, what happened?" | `~/.onlooker/historian/<project-key>/` (embeddings) | SessionEnd, UserPromptSubmit |
|
|
62
|
+
| scribe | "What's a readable artifact of what we did?" | scribe's own output dir | session end |
|
|
63
|
+
| cartographer | "Are the instruction files (CLAUDE.md, AGENTS.md, rules/) internally consistent?" | reads instruction files; emits findings | SessionStart, instruction-file writes |
|
|
64
|
+
| counsel | "What does the *event log* across all plugins tell us to improve?" | reads JSONL log; produces brief | weekly |
|
|
65
|
+
|
|
66
|
+
**The two easiest confusions:**
|
|
67
|
+
|
|
68
|
+
1. **cartographer vs. curator.** Cartographer audits the *instruction files* the user maintains by hand (CLAUDE.md, AGENTS.md, `.claude/rules/`). Curator audits the *typed auto-memory store* the librarian writes into. Same shape of audit, different substrate. They are parallel, not redundant.
|
|
69
|
+
|
|
70
|
+
2. **archivist vs. librarian.** Archivist keeps everything from a session, ranked by recency, and reinjects into the next one. Librarian promotes a small subset into the typed memory store where it will be reinjected *every* session, with classification (user/feedback/project/reference) and provenance.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Shared invariants
|
|
75
|
+
|
|
76
|
+
All three new memory plugins follow the ecosystem conventions:
|
|
77
|
+
|
|
78
|
+
- **Project keying** for plugin-owned storage (`~/.onlooker/<plugin>/<project-key>/`) uses the SHA256-of-remote-URL scheme from [`architecture.md`](architecture.md#project-keying), so cross-clone consistency is preserved for plugin state. Note: the typed memory store the librarian writes into is keyed by Claude Code's per-checkout path encoding (`~/.claude/projects/<encoded-project>/memory/`), which is a different scheme — see [librarian ADR-001](../plugins/librarian/docs/adr/001-propose-dont-auto-write.md) for how this asymmetry is handled.
|
|
79
|
+
- **No cross-plugin runtime dependencies.** Librarian degrades gracefully if archivist artifacts are absent (no proposals; emits `librarian.scan.skipped`). Curator degrades gracefully if the memory store is empty. Historian degrades gracefully if no past transcripts are indexed.
|
|
80
|
+
- **Event emission via `onlooker-event.mjs`.** Event types: `librarian.*`, `curator.*`, `historian.*`. Registered in `@onlooker-community/schema` before first emission.
|
|
81
|
+
- **Fail-soft on missing `~/.onlooker/`.** Plugins must not block a session they were not invited to.
|
|
82
|
+
- **Context-budget awareness.** Reinjection at `SessionStart` competes for the same budget archivist already uses. Each plugin's design lays out its budget cap explicitly; the ecosystem does not yet enforce a global ceiling. See [Open Questions](#open-questions) below.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Open Questions
|
|
87
|
+
|
|
88
|
+
1. **Global reinjection ceiling.** Archivist, librarian (via the typed memory store it writes into), historian, counsel, and cartographer all surface things at `SessionStart`. Each respects its own cap, but there is no shared budget. A `governor.context_budget.*` event series could attribute context spend per plugin and provide a soft global cap. Currently each plugin sets its own ceiling independently.
|
|
89
|
+
|
|
90
|
+
2. **Memory store path scheme divergence.** Claude Code's typed memory store uses per-checkout path encoding; the ecosystem uses SHA256-of-remote-URL for cross-clone sharing. Librarian writes to the former (so the user sees promotions in the same place as their hand-written memories), but its own operational state (proposals queue, provenance log) lives at `~/.onlooker/librarian/<project-key>/` under the ecosystem scheme. This asymmetry is handled in [librarian ADR-001](../plugins/librarian/docs/adr/001-propose-dont-auto-write.md) but remains an awkwardness worth revisiting if Claude Code adopts a remote-derived key.
|
|
91
|
+
|
|
92
|
+
3. **Promotion provenance after deletion.** When the user manually deletes a promoted memory, librarian should learn from that — at minimum, don't re-propose the same content. Sketched in librarian's design as the "tombstone" mechanism but not yet decided.
|
|
93
|
+
|
|
94
|
+
4. **Historian and secrets.** Embedding entire transcripts means tokens, paths, and other sensitive strings that appeared in a past conversation become recallable. Deletion semantics (purge by session_id, by date range, by pattern) need to be defined before historian goes beyond a design sketch.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Plugin design documents
|
|
99
|
+
|
|
100
|
+
- [Librarian](../plugins/librarian/docs/design.md) — consolidates archivist artifacts into the typed memory store
|
|
101
|
+
- [Curator](../plugins/curator/docs/design.md) — detects stale, contradictory, and decayed memories
|
|
102
|
+
- [Historian](../plugins/historian/docs/design.md) — episodic retrieval over past session transcripts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlooker-community/ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Agents, skills, hooks, commands, rules, and MCP configurations that power [Onlooker](https://onlooker.dev)",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"onlooker-install": "install.sh"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@onlooker-community/schema": "^2.
|
|
22
|
+
"@onlooker-community/schema": "^2.5.0"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"postinstall": "echo '\\n onlooker-ecosystem installed!\\n Run: npx onlooker-install typescript\\n Docs: https://github.com/onlooker-community/ecosystem\\n'",
|
|
26
26
|
"test": "npm run test:bats && npm run test:schema",
|
|
27
27
|
"test:bats": "bats test/bats",
|
|
28
28
|
"test:schema": "node --test test/node/*.test.mjs",
|
|
29
|
-
"test:shellcheck": "shellcheck -S error -x install.sh scripts/common.sh scripts/hooks/*.sh scripts/lib/*.sh plugins/archivist/scripts/hooks/*.sh plugins/archivist/scripts/lib/*.sh plugins/tribunal/scripts/hooks/*.sh plugins/tribunal/scripts/lib/*.sh plugins/echo/scripts/hooks/*.sh plugins/echo/scripts/lib/*.sh plugins/governor/scripts/hooks/*.sh plugins/governor/scripts/lib/*.sh plugins/compass/scripts/hooks/*.sh plugins/compass/scripts/lib/*.sh plugins/scribe/scripts/hooks/*.sh plugins/scribe/scripts/lib/*.sh plugins/counsel/scripts/hooks/*.sh plugins/counsel/scripts/lib/*.sh",
|
|
29
|
+
"test:shellcheck": "shellcheck -S error -x install.sh scripts/common.sh scripts/hooks/*.sh scripts/lib/*.sh plugins/archivist/scripts/hooks/*.sh plugins/archivist/scripts/lib/*.sh plugins/tribunal/scripts/hooks/*.sh plugins/tribunal/scripts/lib/*.sh plugins/echo/scripts/hooks/*.sh plugins/echo/scripts/lib/*.sh plugins/governor/scripts/hooks/*.sh plugins/governor/scripts/lib/*.sh plugins/compass/scripts/hooks/*.sh plugins/compass/scripts/lib/*.sh plugins/scribe/scripts/hooks/*.sh plugins/scribe/scripts/lib/*.sh plugins/counsel/scripts/hooks/*.sh plugins/counsel/scripts/lib/*.sh plugins/warden/scripts/hooks/*.sh plugins/warden/scripts/lib/*.sh plugins/librarian/scripts/hooks/*.sh plugins/librarian/scripts/lib/*.sh",
|
|
30
30
|
"lint:references": "node scripts/lint/check-references.mjs",
|
|
31
31
|
"lint:manifests": "node scripts/lint/check-manifests.mjs",
|
|
32
32
|
"coverage:node": "node scripts/coverage/run-coverage.mjs",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ADR-001: Two-Tier Staleness Checks — Cheap Every Session, LLM Weekly
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-06-02
|
|
5
|
+
- Deciders: Meagan
|
|
6
|
+
- Tags: curator, memory, performance, rate-limiting
|
|
7
|
+
|
|
8
|
+
## Context and Problem Statement
|
|
9
|
+
|
|
10
|
+
Curator audits the typed memory store for staleness, broken references, contradictions, and decayed dates. The natural place to surface findings is at `SessionStart` — that's when the user is paged in for the day's work and most receptive to a one-line "Curator: 2 findings to review" pointer.
|
|
11
|
+
|
|
12
|
+
But `SessionStart` fires on every session, including every restart after compaction and every new branch checkout. Whatever curator runs at `SessionStart` runs *a lot*. Two pressures push against each other:
|
|
13
|
+
|
|
14
|
+
- The checks need to be cheap enough not to delay session startup or accumulate hidden cost.
|
|
15
|
+
- The most valuable check (contradiction detection between memory pairs) requires an LLM call per candidate pair, and the candidate set grows with the memory store.
|
|
16
|
+
|
|
17
|
+
A single check tier — either "all checks every session" or "all checks on a manual trigger" — fails the gradient. The first is too expensive at scale; the second is too easy to forget, and findings rot quietly.
|
|
18
|
+
|
|
19
|
+
## Decision Drivers
|
|
20
|
+
|
|
21
|
+
- **`SessionStart` is a hot path.** Cartographer's design explicitly documents that audits run as a detached background process to avoid blocking sessions. Curator inherits the same constraint.
|
|
22
|
+
- **Cheap checks are nearly free.** Date pattern matching, file-exists checks on path references, and `rg` calls for symbol references all sit in the millisecond range. They can run every session inside a wall-clock budget without observable latency.
|
|
23
|
+
- **LLM contradiction checks scale poorly.** Pairwise comparison is O(N²) on the candidate set even after similarity-filtering. A 100-memory store today may have ~10 candidate pairs; a 500-memory store has more. Running this every session is paying a recurring cost that grows with the user's investment in the system — the worst kind of cost curve.
|
|
24
|
+
- **The signal decays slowly.** A contradiction between two memories does not become more or less true between Monday and Tuesday. Weekly cadence captures real changes (new memories, edited memories) without burning compute on a static problem.
|
|
25
|
+
- **User-pull beats system-push for big sweeps.** A manual `/curator scan` skill exists for users who want to force a full sweep — that's the right surface for "I just landed a big refactor; re-check everything."
|
|
26
|
+
- **Cartographer precedent.** Cartographer runs a periodic background audit and surfaces findings as events. Curator's tiered cadence is the moral equivalent: cheap checks are the "always-on" surface, the LLM sweep is the "periodic" surface.
|
|
27
|
+
|
|
28
|
+
## Considered Options
|
|
29
|
+
|
|
30
|
+
1. **One tier: all checks every session.** Simple, predictable, expensive at scale. Wall-clock budget acts as a ceiling but degrades coverage as the store grows.
|
|
31
|
+
2. **One tier: all checks on manual trigger only.** Cheap and predictable but loses the always-on signal users want from `SessionStart`. Findings rot.
|
|
32
|
+
3. **Two tiers: cheap checks every session, LLM sweep at most once per N days.** Preserves the always-on cheap signal and amortizes the expensive sweep over time.
|
|
33
|
+
4. **Three tiers: cheap every session, mid-cost (e.g., rg-based symbol re-check) daily, LLM weekly.** Finer granularity. Adds operational complexity (multiple watermarks, multiple skip-reason cases) without an obvious payoff at current memory-store sizes.
|
|
34
|
+
5. **Adaptive cadence.** Run the LLM sweep when the memory store has changed by more than X% since last sweep. Conceptually elegant but introduces a change-detection layer that itself needs validation.
|
|
35
|
+
|
|
36
|
+
## Decision
|
|
37
|
+
|
|
38
|
+
We adopt **Option 3: two tiers with a watermark-gated LLM sweep at most once per `llm_sweep_interval_days` (default: 7)**.
|
|
39
|
+
|
|
40
|
+
The cheap tier runs on every `SessionStart` inside a `wall_clock_budget_ms` budget (default: 500ms). It performs:
|
|
41
|
+
|
|
42
|
+
- Date pattern parsing and grace-period checks.
|
|
43
|
+
- Path reference existence checks.
|
|
44
|
+
- Symbol reference `rg` checks (capped at `max_symbol_checks_per_session`).
|
|
45
|
+
- Usage tracker reads from the JSONL log (when the `memory.recalled` emitter ships).
|
|
46
|
+
|
|
47
|
+
If the cheap tier runs over budget, curator emits `curator.scan.skipped` with `reason: "over_budget"` and exits without partial results. The wall-clock budget exists to make budget overruns visible as events rather than as silent slowdowns.
|
|
48
|
+
|
|
49
|
+
The LLM tier runs only when `now - last_llm_sweep_at >= llm_sweep_interval_days`. The watermark lives at `~/.onlooker/curator/<project-key>/last_llm_sweep.json`. When the gate opens:
|
|
50
|
+
|
|
51
|
+
- Pairwise Jaccard similarity is computed across all memories.
|
|
52
|
+
- Pairs above `contradiction_similarity_threshold` (default: 0.4) with opposing sentiment markers proceed to LLM evaluation.
|
|
53
|
+
- Up to `max_pair_evaluations_per_sweep` (default: 50) calls are made, watermark advances regardless of whether the cap is hit.
|
|
54
|
+
|
|
55
|
+
The manual `/curator scan` skill bypasses both rate gates. Useful for "I just landed N memories; re-check now."
|
|
56
|
+
|
|
57
|
+
Option 4 was rejected because the user-visible benefit of a third tier is unclear at current memory-store sizes (typically tens of entries, not hundreds). It can be added later without changing the architecture if scale changes the calculus.
|
|
58
|
+
|
|
59
|
+
Option 5 was rejected as a default because change-detection introduces a calibration problem (what counts as significant change?) on top of the staleness-detection problem. It is plausible as a future optimization layered on Option 3 (e.g., advance the LLM watermark when the memory store has changed by less than a threshold, to defer the next sweep).
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
|
|
63
|
+
### Positive
|
|
64
|
+
|
|
65
|
+
- `SessionStart` latency stays bounded by the cheap-check wall-clock budget — typically well under 500ms.
|
|
66
|
+
- The expensive LLM sweep runs at a cadence that matches the rate-of-change of the underlying signal (memories don't contradict each other on a per-minute basis).
|
|
67
|
+
- A manual override (`/curator scan`) gives users the always-available "scan now" surface without making it the default.
|
|
68
|
+
- Budget overruns are observable: `curator.scan.skipped` with `reason: "over_budget"` is a leading indicator that the memory store has grown enough to need tuning.
|
|
69
|
+
- The watermark-gated sweep also caps cost: a user with a single high-traffic project pays at most N LLM-sweep calls per `llm_sweep_interval_days`.
|
|
70
|
+
|
|
71
|
+
### Negative
|
|
72
|
+
|
|
73
|
+
- A contradiction introduced today may go up to 7 days before being flagged. This is the cost of weekly cadence. Mitigation: the manual `/curator scan` exists; users who notice they just added two conflicting memories can force a check.
|
|
74
|
+
- The two-tier model is slightly more complex than one tier. Two watermarks (cheap-tier last-run, LLM-tier last-sweep), two budget knobs, two skip reasons. The added complexity is small relative to the cost savings.
|
|
75
|
+
- The cheap tier's wall-clock budget interacts badly with very large memory stores. At ≥200 memories, the cheap tier may itself need a per-memory cap (e.g., "scan only memories touched in the last N days"). Not yet a problem; flagged as a future open question.
|
|
76
|
+
|
|
77
|
+
### Neutral
|
|
78
|
+
|
|
79
|
+
- The choice of 7 days for `llm_sweep_interval_days` is a guess. Users who care can override. A future calibration could measure how often pair-similarity-with-opposing-sentiment results change verdict between consecutive sweeps; if the rate is low, the interval could grow.
|
|
80
|
+
|
|
81
|
+
## Implementation Notes
|
|
82
|
+
|
|
83
|
+
- The cheap-tier wall-clock budget is checked between sub-tasks, not within them. A single `rg` call that itself runs over budget is allowed to finish; the gate prevents *the next* sub-task from starting.
|
|
84
|
+
- The LLM watermark is updated *before* the sweep begins, not after. This prevents a sweep that crashes midway from being retried immediately on the next `SessionStart`. The downside: a crashed sweep delays the next full check by the interval. Acceptable — better than a crash-retry loop.
|
|
85
|
+
- Findings dedup uses `deduped_hash`. For `contradiction` findings the hash includes both memory bodies' content hashes, so an edit to either body re-opens the finding for re-evaluation on the next sweep.
|
|
86
|
+
- Manual `/curator scan` updates the watermark like an automatic sweep — a user who runs the skill on Monday and Tuesday gets two LLM sweeps in two days, which is fine because they explicitly asked for it.
|
|
87
|
+
- The cheap-tier and LLM-tier are independently configurable via `cheap_checks.enabled` and `llm_sweep.enabled`. Users can run either tier alone (e.g., LLM sweep off in a budget-sensitive project).
|
|
88
|
+
|
|
89
|
+
## Validation
|
|
90
|
+
|
|
91
|
+
- A memory store of ~50 entries should produce cheap-check sweeps under 200ms on a typical macOS dev laptop. If `curator.scan.skipped` with `reason: "over_budget"` fires at this size, the wall-clock budget needs tuning, not the design.
|
|
92
|
+
- An LLM sweep at ~50 memories should make ≤20 pair-evaluation calls in the common case. The `max_pair_evaluations_per_sweep` cap of 50 should not be reached. If it is reached regularly, similarity threshold and sentiment-marker filtering need tuning.
|
|
93
|
+
- Findings open for more than two LLM-sweep intervals without user action should produce a `curator.finding.aged_unhandled` summary event — an integration point for counsel's weekly brief.
|
|
94
|
+
|
|
95
|
+
## References
|
|
96
|
+
|
|
97
|
+
- Cartographer design — precedent for background audits with per-finding events (`plugins/cartographer/docs/design.md`)
|
|
98
|
+
- Compass ADR-001 — precedent for explicit budget gates and skip-reason events (`plugins/compass/docs/adr/001-evaluate-prompts-in-context.md`)
|
|
99
|
+
- Memory architecture overview (`docs/memory-architecture.md`)
|
|
100
|
+
- Curator design (`../design.md`)
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Curator — Plugin Design
|
|
2
|
+
|
|
3
|
+
**Plugin name:** `curator`
|
|
4
|
+
**Tagline:** *Tends the memory garden.*
|
|
5
|
+
**Status:** Design (pre-implementation)
|
|
6
|
+
|
|
7
|
+
Curator is the maintenance layer for the user's typed memory store. It runs cheap heuristic checks at every `SessionStart` and an LLM-backed conflict sweep at most weekly, surfaces stale references, decayed dates, and contradicting entries, and proposes prunes for user review. It does not edit the memory store directly — the same posture librarian and cartographer adopt for durable substrates.
|
|
8
|
+
|
|
9
|
+
It sits in the [memory architecture](../../../docs/memory-architecture.md) downstream of librarian: librarian writes (with user confirmation); curator audits. Curator is parallel to cartographer: same shape (audit, propose, surface), different substrate. Cartographer audits hand-maintained instruction files (CLAUDE.md, AGENTS.md, `.claude/rules/`); curator audits the typed auto-memory store at `~/.claude/projects/<encoded-project>/memory/`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Failure Modes Curator Addresses
|
|
14
|
+
|
|
15
|
+
**A — Decayed date references.** A project memory says "merge freeze begins 2026-03-05 for mobile release cut." After March 5 passes, the memory is at best uninformative and at worst misleading (the model continues to flag work as freeze-sensitive). Curator detects past-tense date markers and proposes removal or refactor.
|
|
16
|
+
|
|
17
|
+
**B — Stale path references.** A reference memory says "see `scripts/legacy_ingest.py` for the old pipeline shape." The file has since been deleted. The memory now points to nothing. Curator validates path references on a periodic sweep and flags broken ones.
|
|
18
|
+
|
|
19
|
+
**C — Contradicting memories.** A user memory says "prefer functional patterns" and a feedback memory says "yes, the class-based approach was right for this hot path." Both are true in their original contexts. The model has to reconcile them at runtime, often badly. Curator's LLM-backed sweep finds high-similarity, opposing-sentiment pairs and surfaces the contradiction for human disambiguation.
|
|
20
|
+
|
|
21
|
+
**D — Unused memories (weakest signal).** A memory has been in the store for 90 days and has never been surfaced as relevant in any session (signal: no `memory.recalled` event references it). It might be load-bearing as a backstop, or it might be dead weight. Curator flags but does not propose removal — the signal is too noisy for action.
|
|
22
|
+
|
|
23
|
+
**E — Type drift.** A `project` memory ("we're rewriting auth for compliance") becomes a `feedback` memory ("this directory looks weird because of legal review") once the rewrite is done. The original type still fits but a better type now exists. Curator can detect type-drift candidates but the action (re-classification) is necessarily manual.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
SessionStart hook fires
|
|
31
|
+
│
|
|
32
|
+
▼
|
|
33
|
+
┌──────────────────────┐
|
|
34
|
+
│ Rate Gate │ cheap checks: every session
|
|
35
|
+
│ │ LLM checks: once per llm_sweep_interval_days
|
|
36
|
+
└─────────┬────────────┘
|
|
37
|
+
│
|
|
38
|
+
▼
|
|
39
|
+
┌──────────────────────┐
|
|
40
|
+
│ Memory Reader │ reads MEMORY.md + *.md files from memory store
|
|
41
|
+
│ │ parses frontmatter (name, description, type)
|
|
42
|
+
└─────────┬────────────┘
|
|
43
|
+
│
|
|
44
|
+
▼ (cheap sweep, every session)
|
|
45
|
+
┌──────────────────────┐
|
|
46
|
+
│ Date Checker │ parse dates from bodies; flag past-tense markers
|
|
47
|
+
└─────────┬────────────┘
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
┌──────────────────────┐
|
|
51
|
+
│ Reference Checker │ validate path refs (file exists), symbol refs
|
|
52
|
+
│ │ (rg the symbol; warn on zero matches), URL refs
|
|
53
|
+
│ │ (HEAD with budget; skipped without consent)
|
|
54
|
+
└─────────┬────────────┘
|
|
55
|
+
│
|
|
56
|
+
▼
|
|
57
|
+
┌──────────────────────┐
|
|
58
|
+
│ Usage Tracker │ read JSONL log; correlate memory IDs with
|
|
59
|
+
│ │ memory.recalled events from N days
|
|
60
|
+
└─────────┬────────────┘
|
|
61
|
+
│
|
|
62
|
+
▼ (LLM sweep, if interval elapsed)
|
|
63
|
+
┌──────────────────────┐
|
|
64
|
+
│ Similarity Matrix │ Jaccard on token sets; pairs with sim > threshold
|
|
65
|
+
│ │ → LLM contradiction check
|
|
66
|
+
└─────────┬────────────┘
|
|
67
|
+
│
|
|
68
|
+
▼
|
|
69
|
+
┌──────────────────────┐
|
|
70
|
+
│ Findings Store │ ~/.onlooker/curator/<key>/findings/<ulid>.json
|
|
71
|
+
└─────────┬────────────┘
|
|
72
|
+
│ at SessionStart
|
|
73
|
+
▼
|
|
74
|
+
┌──────────────────────┐
|
|
75
|
+
│ Surfacer │ "Curator: 2 stale, 1 contradicting findings."
|
|
76
|
+
│ │ Review via /curator review.
|
|
77
|
+
└──────────────────────┘
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Rate Gate
|
|
81
|
+
|
|
82
|
+
Three categories of check, three cadences:
|
|
83
|
+
|
|
84
|
+
- **Cheap checks (date, reference, usage):** run every `SessionStart`. Combined wall-clock budget: ≤500ms. Above that, curator emits `curator.scan.skipped` with `reason: "over_budget"` and defers.
|
|
85
|
+
- **LLM contradiction sweep:** runs at most once per `llm_sweep_interval_days` (default: 7) per project. Watermark stored at `~/.onlooker/curator/<project-key>/last_llm_sweep.json`.
|
|
86
|
+
- **Manual sweep:** `/curator scan` forces a full sweep including the LLM pass, ignoring rate gates.
|
|
87
|
+
|
|
88
|
+
The rate gate exists because curator runs on every session start, and a quadratic LLM pass on a growing memory store is the worst kind of background cost: invisible, recurring, and proportional to user investment.
|
|
89
|
+
|
|
90
|
+
### Memory Reader
|
|
91
|
+
|
|
92
|
+
Parses the typed memory store:
|
|
93
|
+
|
|
94
|
+
1. Reads `~/.claude/projects/<encoded-project>/memory/MEMORY.md` for the index entries.
|
|
95
|
+
2. For each line of the form `- [Title](file.md) — hook`, resolves `file.md` against the memory dir.
|
|
96
|
+
3. Reads each referenced file. Parses YAML frontmatter (`name`, `description`, `type`). The body after frontmatter is the memory content.
|
|
97
|
+
4. If a file is referenced from `MEMORY.md` but does not exist, that itself is a `findings.broken_index` — surfaced immediately.
|
|
98
|
+
5. If a file exists in the memory dir but is not referenced from `MEMORY.md`, that is `findings.orphaned_memory` — also surfaced.
|
|
99
|
+
|
|
100
|
+
### Date Checker
|
|
101
|
+
|
|
102
|
+
For each memory body, scans for date patterns and absolute references:
|
|
103
|
+
|
|
104
|
+
- **ISO-8601 dates** (`2026-03-05`, `2026-03-05T10:00:00Z`).
|
|
105
|
+
- **Quarter markers** (`Q1 2026`, `2026Q3`).
|
|
106
|
+
- **Named deadlines** with absolute dates nearby (`freeze`, `deadline`, `release cut`, `migration`, `cutover`, `EOL`, `expires`).
|
|
107
|
+
- **Relative-to-write markers** when the frontmatter has a discoverable write date (`promoted_at`, `created_at`): phrases like "next week", "by end of month", "this Friday" relative to that date.
|
|
108
|
+
|
|
109
|
+
For each match, compares to today's date. If a date is more than `date_grace_period_days` (default: 14) in the past, emits `curator.finding.date_decayed` with the matched phrase and the gap in days.
|
|
110
|
+
|
|
111
|
+
The check does not propose removal automatically — past dates often have lingering relevance ("freeze on 2026-03-05" might still document why a code shape is the way it is). The user decides whether to remove, refactor, or keep.
|
|
112
|
+
|
|
113
|
+
### Reference Checker
|
|
114
|
+
|
|
115
|
+
For each memory body, scans for two kinds of references:
|
|
116
|
+
|
|
117
|
+
1. **Path references.** Patterns matching `path/to/file.ext` heuristics. For each candidate path, resolves against the repo root (from `git rev-parse --show-toplevel`). If the path does not exist, emits `curator.finding.path_broken` with the memory file and the broken path.
|
|
118
|
+
2. **Symbol references.** Heuristic: backtick-wrapped identifiers (`` `myFunction` ``, `` `MyClass` ``) that look like code identifiers (CamelCase or snake_case with no spaces, length ≥ 3). For each, runs `rg --type-add 'all:*' --type all -F 'identifier'` in the repo root. If zero matches, emits `curator.finding.symbol_missing`.
|
|
119
|
+
3. **URL references.** Optional, disabled by default. When `check_urls: true` and the URL host is not in `url_allowlist`, curator emits `curator.finding.url_unchecked` (a record that the memory contains an external URL it cannot validate without network). URLs in the allowlist (and only those) are HEAD-checked under a wall-clock budget.
|
|
120
|
+
|
|
121
|
+
The reference checker treats matches as evidence of liveness, not correctness. A symbol that grep-matches might still be the wrong symbol; a path that resolves might point to renamed content. The checker is a smoke alarm, not a smoke detector.
|
|
122
|
+
|
|
123
|
+
### Usage Tracker
|
|
124
|
+
|
|
125
|
+
Reads `~/.onlooker/logs/onlooker-events.jsonl` (rate-limited; the tail is enough for usage windows) for events of type `memory.recalled` and `memory.referenced` over the last `usage_window_days` (default: 30). For each memory file, computes recall count.
|
|
126
|
+
|
|
127
|
+
The Onlooker event log does not yet emit `memory.recalled` events. Adding that emitter belongs to the ecosystem substrate (so all plugins benefit), not to curator. Until it ships, the usage tracker emits `curator.finding.unused_undetectable` once per scan and skips the rest of the pass. This is recorded as a hard dependency in [Open Questions #1](#open-questions).
|
|
128
|
+
|
|
129
|
+
When the emitter ships: memories with zero recalls in the window are flagged `curator.finding.unused_low_signal`. The finding is informational only — the design does not propose removal based on usage alone, because the recall signal is itself noisy (the model may not surface a memory it should have, and a recalled memory may have been irrelevant).
|
|
130
|
+
|
|
131
|
+
### Similarity Matrix and Contradiction Check (LLM sweep)
|
|
132
|
+
|
|
133
|
+
Run at most once per `llm_sweep_interval_days`:
|
|
134
|
+
|
|
135
|
+
1. Compute pairwise Jaccard similarity over normalized token sets (lowercased, stopwords removed, top-K tokens per body).
|
|
136
|
+
2. Filter to pairs where similarity ≥ `contradiction_similarity_threshold` (default: 0.4) and where the two memories have at least one opposing sentiment marker (one contains `always`/`prefer`/`do` and the other contains `never`/`avoid`/`don't`).
|
|
137
|
+
3. For each surviving pair, call Haiku with both memory bodies and ask:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
You are evaluating whether two memory entries contradict each other in practice.
|
|
141
|
+
|
|
142
|
+
Two memories CONTRADICT when applying both leads to inconsistent action.
|
|
143
|
+
Two memories COMPLEMENT when they apply in different contexts and a careful reader
|
|
144
|
+
can follow both.
|
|
145
|
+
Two memories are REDUNDANT when one strictly subsumes the other.
|
|
146
|
+
|
|
147
|
+
RULES:
|
|
148
|
+
- Output only: {"verdict": "<contradict|complement|redundant|unrelated>",
|
|
149
|
+
"rationale": "<≤30 words>"}
|
|
150
|
+
|
|
151
|
+
<memory_a>
|
|
152
|
+
title: {{TITLE_A}}
|
|
153
|
+
body: {{BODY_A}}
|
|
154
|
+
</memory_a>
|
|
155
|
+
|
|
156
|
+
<memory_b>
|
|
157
|
+
title: {{TITLE_B}}
|
|
158
|
+
body: {{BODY_B}}
|
|
159
|
+
</memory_b>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Model: `claude-haiku-4-5-20251001`. Temperature 0.2. Max output tokens: 96.
|
|
163
|
+
|
|
164
|
+
`contradict` verdicts become `curator.finding.contradiction`. `redundant` verdicts become `curator.finding.redundant_pair`. `complement` and `unrelated` are logged but not surfaced.
|
|
165
|
+
|
|
166
|
+
### Findings Store and Surfacer
|
|
167
|
+
|
|
168
|
+
Each finding is written to `~/.onlooker/curator/<project-key>/findings/<ulid>.json`:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"id": "01J...",
|
|
173
|
+
"kind": "date_decayed | path_broken | symbol_missing | url_unchecked | unused_low_signal | contradiction | redundant_pair | broken_index | orphaned_memory",
|
|
174
|
+
"memory_files": ["feedback_no_trailing_summaries.md"],
|
|
175
|
+
"detail": { ... kind-specific ... },
|
|
176
|
+
"created_at": "2026-06-02T18:24:11Z",
|
|
177
|
+
"deduped_hash": "...",
|
|
178
|
+
"status": "open | acknowledged | resolved"
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The `deduped_hash` prevents the same finding from being re-emitted every session. Same shape as cartographer's `payload.finding_hash`.
|
|
183
|
+
|
|
184
|
+
At `SessionStart`, curator counts open findings by kind and emits a one-line `additionalContext` pointer:
|
|
185
|
+
|
|
186
|
+
> Curator: 1 contradiction, 2 path-broken, 1 date-decayed. Review with `/curator review`.
|
|
187
|
+
|
|
188
|
+
The pointer caps the inject at one line; findings details live in the skill, not in context.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Integration Points
|
|
193
|
+
|
|
194
|
+
**Librarian.** Curator uses the `source: "librarian"` provenance to apply different staleness criteria to librarian-promoted memories vs. hand-written ones (open question — current default treats them identically).
|
|
195
|
+
|
|
196
|
+
**Cartographer.** Same shape; different substrate. They can run independently. Curator's findings format intentionally mirrors cartographer's so a future unified findings dashboard can render both.
|
|
197
|
+
|
|
198
|
+
**Ecosystem substrate.** Curator depends on a `memory.recalled` / `memory.referenced` event emitter that does not yet exist. Until it ships, the usage tracker is dormant.
|
|
199
|
+
|
|
200
|
+
**Counsel.** Counsel reads curator's findings as part of the weekly observability brief; curator does not need to know about counsel.
|
|
201
|
+
|
|
202
|
+
**Historian.** Independent. Curator audits the distilled memory store; historian operates on the transcript embeddings. A path that's stale in a memory is not made fresh by being in a transcript.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Configuration (`config.json`)
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"plugin_name": "curator",
|
|
211
|
+
"storage_path": "${ONLOOKER_DIR:-$HOME/.onlooker}",
|
|
212
|
+
"curator": {
|
|
213
|
+
"enabled": false,
|
|
214
|
+
"memory_store_path": "${HOME}/.claude/projects/${CLAUDE_PROJECT_ENCODED}/memory",
|
|
215
|
+
"cheap_checks": {
|
|
216
|
+
"enabled": true,
|
|
217
|
+
"wall_clock_budget_ms": 500,
|
|
218
|
+
"skip_if_session_age_under_seconds": 5
|
|
219
|
+
},
|
|
220
|
+
"date_check": {
|
|
221
|
+
"enabled": true,
|
|
222
|
+
"date_grace_period_days": 14
|
|
223
|
+
},
|
|
224
|
+
"reference_check": {
|
|
225
|
+
"enabled": true,
|
|
226
|
+
"check_urls": false,
|
|
227
|
+
"url_allowlist": []
|
|
228
|
+
},
|
|
229
|
+
"usage_tracker": {
|
|
230
|
+
"enabled": true,
|
|
231
|
+
"usage_window_days": 30
|
|
232
|
+
},
|
|
233
|
+
"llm_sweep": {
|
|
234
|
+
"enabled": true,
|
|
235
|
+
"model": "claude-haiku-4-5-20251001",
|
|
236
|
+
"temperature": 0.2,
|
|
237
|
+
"max_output_tokens": 96,
|
|
238
|
+
"interval_days": 7,
|
|
239
|
+
"max_pair_evaluations_per_sweep": 50,
|
|
240
|
+
"contradiction_similarity_threshold": 0.40
|
|
241
|
+
},
|
|
242
|
+
"surfacer": {
|
|
243
|
+
"max_pointer_chars": 200,
|
|
244
|
+
"skip_when_zero": true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`skip_if_session_age_under_seconds` exists because a session start followed quickly by another session start (compaction, restart) shouldn't re-run the cheap checks.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Events
|
|
255
|
+
|
|
256
|
+
| Event | Trigger | Key payload fields |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `curator.scan.started` | Scan run begins | `mode: cheap\|llm\|manual`, `findings_open_before` |
|
|
259
|
+
| `curator.scan.completed` | Scan run ends | `findings_new`, `findings_resolved`, `duration_ms` |
|
|
260
|
+
| `curator.scan.skipped` | Skipped by rate gate | `reason: over_budget\|llm_interval_not_elapsed\|disabled` |
|
|
261
|
+
| `curator.finding.date_decayed` | A dated phrase is past the grace period | `memory_file`, `matched_phrase`, `days_past` |
|
|
262
|
+
| `curator.finding.path_broken` | Path reference does not resolve | `memory_file`, `broken_path` |
|
|
263
|
+
| `curator.finding.symbol_missing` | Backticked identifier returns zero rg matches | `memory_file`, `symbol` |
|
|
264
|
+
| `curator.finding.url_unchecked` | URL present, host not in allowlist | `memory_file`, `url_host` |
|
|
265
|
+
| `curator.finding.unused_low_signal` | Zero recalls in window (when emitter exists) | `memory_file`, `window_days` |
|
|
266
|
+
| `curator.finding.unused_undetectable` | Usage emitter not present | `note: "memory.recalled events not implemented"` |
|
|
267
|
+
| `curator.finding.contradiction` | LLM verdict `contradict` | `memory_a`, `memory_b`, `rationale` |
|
|
268
|
+
| `curator.finding.redundant_pair` | LLM verdict `redundant` | `memory_a`, `memory_b`, `rationale` |
|
|
269
|
+
| `curator.finding.broken_index` | MEMORY.md references missing file | `referenced_file` |
|
|
270
|
+
| `curator.finding.orphaned_memory` | Memory file not referenced from MEMORY.md | `memory_file` |
|
|
271
|
+
| `curator.finding.acknowledged` | User acknowledged finding via skill (no action taken) | `finding_id` |
|
|
272
|
+
| `curator.finding.resolved` | User resolved finding via skill (action taken) | `finding_id`, `action: prune\|edit\|reclassify\|defer` |
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Skills
|
|
277
|
+
|
|
278
|
+
**`/curator review`** — interactive walkthrough of open findings. For each: shows the memory body excerpt, the finding kind and detail, and offers prune / edit / reclassify / acknowledge / defer.
|
|
279
|
+
|
|
280
|
+
**`/curator scan`** — forces a full sweep including the LLM pass. Ignores rate gates.
|
|
281
|
+
|
|
282
|
+
**`/curator calibrate`** — runs the LLM sweep against the current memory store and reports precision against a labeled set (which the user maintains in `~/.onlooker/curator/<project-key>/calibration_labels.json`). Useful for tuning `contradiction_similarity_threshold`.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Open Questions
|
|
287
|
+
|
|
288
|
+
1. **`memory.recalled` event dependency.** The usage tracker requires an event emitter in the ecosystem substrate that does not yet exist. The substrate change is small (`UserPromptExpansion` hook can emit an event each time a memory is reinjected) but it is a prerequisite. Until then, the usage signal is dormant — `curator.finding.unused_undetectable` is emitted once per scan to make the missing capability visible.
|
|
289
|
+
|
|
290
|
+
2. **Librarian-promoted vs. hand-written staleness.** A librarian-promoted memory was distilled from a session; its staleness criteria might be "the source session is older than X." A hand-written memory has no equivalent decay marker. The current design treats them identically; the provenance field is captured but not yet used differently.
|
|
291
|
+
|
|
292
|
+
3. **LLM sweep cost growth.** Pairwise contradiction checks are O(N²) on pair candidates. At 100 memories with similarity-filtering, the sweep is typically under 10 LLM calls; at 500 memories the worst case approaches the `max_pair_evaluations_per_sweep` cap. A smarter pre-filter (e.g., embedding-based clustering to limit pair candidates) becomes worthwhile around 200 memories.
|
|
293
|
+
|
|
294
|
+
4. **Finding dedup vs. re-evaluation.** A `date_decayed` finding for `2026-03-05` is the same fact every session — `deduped_hash` prevents re-emission. But a `contradiction` finding between two memories may be re-evaluated if either memory's body changes; the dedup hash should include both bodies' hashes, not just memory IDs.
|
|
295
|
+
|
|
296
|
+
5. **Auto-prune as a future opt-in.** Like librarian's `auto_promote`, curator could grow an `auto_prune` mode for high-confidence findings (e.g., `path_broken` with no possible interpretation). Deferred until the cheap-check precision is measured in practice.
|
|
297
|
+
|
|
298
|
+
6. **Type-drift detection.** Mentioned as failure mode E but not addressed by the current checks. Would require an LLM call per memory: "given this body, what type fits best?" — too expensive for every session, plausible for the weekly sweep.
|
|
299
|
+
|
|
300
|
+
7. **Interaction with `~/.claude/CLAUDE.md`.** Global instructions in `~/.claude/CLAUDE.md` shape behavior but live outside the typed memory store. Curator does not audit them — cartographer does. If the boundary moves (e.g., librarian gains the ability to propose `~/.claude/CLAUDE.md` edits), curator and cartographer will need a shared rule for which substrate owns which file.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Non-Goals
|
|
305
|
+
|
|
306
|
+
- Does not edit the memory store automatically — same posture as librarian and cartographer.
|
|
307
|
+
- Does not write new memories — that is librarian's job.
|
|
308
|
+
- Does not perform retrieval — the typed memory store reinjection mechanism is owned elsewhere.
|
|
309
|
+
- Does not audit instruction files (CLAUDE.md, AGENTS.md, `.claude/rules/`) — that is cartographer's job.
|
|
310
|
+
- Does not synthesize cross-session improvement briefs — that is counsel's job.
|
|
311
|
+
- Does not block any tool call — curator's surfacer is informational only.
|