@onlooker-community/ecosystem 0.20.0 → 0.21.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 +26 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +3 -2
- package/CHANGELOG.md +7 -0
- package/package.json +2 -2
- package/plugins/curator/.claude-plugin/plugin.json +14 -0
- package/plugins/curator/CHANGELOG.md +10 -0
- package/plugins/curator/README.md +55 -0
- package/plugins/curator/config.json +41 -0
- package/plugins/curator/hooks/hooks.json +15 -0
- package/plugins/curator/scripts/hooks/curator-session-start.sh +343 -0
- package/plugins/curator/scripts/lib/curator-checks.sh +155 -0
- package/plugins/curator/scripts/lib/curator-config.sh +67 -0
- package/plugins/curator/scripts/lib/curator-emit.sh +61 -0
- package/plugins/curator/scripts/lib/curator-memory-reader.sh +225 -0
- package/plugins/curator/scripts/lib/curator-project-key.sh +82 -0
- package/plugins/curator/scripts/lib/curator-storage.sh +176 -0
- package/plugins/curator/scripts/lib/curator-ulid.sh +43 -0
- package/release-please-config.json +16 -0
- package/test/bats/curator-session-start.bats +316 -0
|
@@ -124,6 +124,32 @@
|
|
|
124
124
|
"license": "MIT",
|
|
125
125
|
"keywords": ["security", "prompt-injection", "rule-of-two", "safety", "content-gate", "untrusted-content"],
|
|
126
126
|
"tags": ["safety", "security"]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "librarian",
|
|
130
|
+
"source": "./plugins/librarian",
|
|
131
|
+
"description": "Consolidation layer between archivist's per-session artifacts and the user's durable typed memory store. Reads archivist artifacts at SessionEnd, applies a durability filter, classifies survivors via Haiku into the four memory types (user, feedback, project, reference), and queues proposals for explicit confirmation via /librarian review. Auto-promotion is opt-in. Requires the ecosystem plugin.",
|
|
132
|
+
"author": {
|
|
133
|
+
"name": "Onlooker Community"
|
|
134
|
+
},
|
|
135
|
+
"homepage": "https://onlooker.dev",
|
|
136
|
+
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
137
|
+
"license": "MIT",
|
|
138
|
+
"keywords": ["memory", "consolidation", "promotion", "classifier", "auto-memory", "session"],
|
|
139
|
+
"tags": ["memory", "context-engineering"]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "curator",
|
|
143
|
+
"source": "./plugins/curator",
|
|
144
|
+
"description": "Maintenance layer for the typed auto-memory store. At every SessionStart, runs four cheap heuristic checks against the memories at ~/.claude/projects/<encoded>/memory/ — date_decayed (ISO-8601 dates past the grace period), path_broken (path-shaped references that don't resolve under the repo root), broken_index (MEMORY.md pointing at missing files), and orphaned_memory (files in the dir not referenced from MEMORY.md). Surfaces findings via /curator review; never edits the memory store directly. Parallel to cartographer (which audits hand-maintained instruction files), curator audits the auto-memory substrate. Requires the ecosystem plugin.",
|
|
145
|
+
"author": {
|
|
146
|
+
"name": "Onlooker Community"
|
|
147
|
+
},
|
|
148
|
+
"homepage": "https://onlooker.dev",
|
|
149
|
+
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
150
|
+
"license": "MIT",
|
|
151
|
+
"keywords": ["memory", "audit", "staleness", "findings", "auto-memory", "decay"],
|
|
152
|
+
"tags": ["memory", "context-engineering"]
|
|
127
153
|
}
|
|
128
154
|
]
|
|
129
155
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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.21.0",
|
|
3
3
|
"plugins/archivist": "0.1.0",
|
|
4
4
|
"plugins/tribunal": "1.0.1",
|
|
5
5
|
"plugins/echo": "0.2.0",
|
|
@@ -9,5 +9,6 @@
|
|
|
9
9
|
"plugins/scribe": "0.2.0",
|
|
10
10
|
"plugins/counsel": "0.2.0",
|
|
11
11
|
"plugins/warden": "0.2.0",
|
|
12
|
-
"plugins/librarian": "0.1.0"
|
|
12
|
+
"plugins/librarian": "0.1.0",
|
|
13
|
+
"plugins/curator": "0.1.0"
|
|
13
14
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.21.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.20.0...ecosystem-v0.21.0) (2026-06-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **curator:** introduce plugin with cheap-tier checks :microscope: ([#57](https://github.com/onlooker-community/ecosystem/issues/57)) ([7f9fa18](https://github.com/onlooker-community/ecosystem/commit/7f9fa18bbde29c8b5bd1eaad185bd4c5595a3762))
|
|
9
|
+
|
|
3
10
|
## [0.20.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.19.0...ecosystem-v0.20.0) (2026-06-04)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlooker-community/ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|
|
@@ -26,7 +26,7 @@
|
|
|
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 plugins/warden/scripts/hooks/*.sh plugins/warden/scripts/lib/*.sh plugins/librarian/scripts/hooks/*.sh plugins/librarian/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 plugins/curator/scripts/hooks/*.sh plugins/curator/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,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "curator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Maintenance layer for the user's typed auto-memory store. At every SessionStart, runs four cheap heuristic checks (date_decayed, path_broken, broken_index, orphaned_memory) against the memories at ~/.claude/projects/<encoded>/memory/ inside a wall-clock budget. Surfaces findings as a one-line pointer to /curator review; never edits the memory store directly. Builds on the Onlooker ecosystem plugin.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Onlooker Community",
|
|
7
|
+
"url": "https://onlooker.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://onlooker.dev",
|
|
10
|
+
"repository": "https://github.com/onlooker-community/ecosystem",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"skills": [],
|
|
13
|
+
"agents": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0](https://github.com/onlooker-community/ecosystem/compare/curator-v0.0.1...curator-v0.1.0) (2026-06-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **curator:** introduce plugin with cheap-tier checks :microscope: ([#57](https://github.com/onlooker-community/ecosystem/issues/57)) ([7f9fa18](https://github.com/onlooker-community/ecosystem/commit/7f9fa18bbde29c8b5bd1eaad185bd4c5595a3762))
|
|
9
|
+
|
|
10
|
+
## Changelog
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Curator
|
|
2
|
+
|
|
3
|
+
Maintenance layer for the user's typed auto-memory store.
|
|
4
|
+
|
|
5
|
+
At every `SessionStart`, Curator runs cheap heuristic checks against the memories at `~/.claude/projects/<encoded-project>/memory/` — stale ISO-8601 dates past the grace period, broken path references, broken `MEMORY.md` index entries, and orphaned memory files — and surfaces findings as a one-line pointer to `/curator review`. Curator never edits the memory store directly.
|
|
6
|
+
|
|
7
|
+
The weekly LLM-backed contradiction sweep described in [`docs/design.md`](docs/design.md) is a future capability — see the **Status** section below for what's actually shipped today.
|
|
8
|
+
|
|
9
|
+
Curator is a sibling plugin to [`ecosystem`](../../) and assumes the Onlooker observability substrate (`~/.onlooker/`) is present. It is parallel to [`cartographer`](../cartographer) (which audits hand-maintained instruction files like `CLAUDE.md`) — same audit shape, different substrate.
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
| Hook | What Curator does |
|
|
14
|
+
|------|---------------------|
|
|
15
|
+
| `SessionStart` | Runs the four cheap-tier checks (date_decayed, path_broken, broken_index, orphaned_memory) against the memory store, inside a wall-clock budget (`cheap_checks.wall_clock_budget_ms`, default 500ms). Writes new findings under `~/.onlooker/curator/<project-key>/findings/` keyed by ULID, deduping repeat findings via `deduped_hash`. Injects a one-line `additionalContext` pointer when open findings exist. |
|
|
16
|
+
|
|
17
|
+
## Activation
|
|
18
|
+
|
|
19
|
+
Curator is **off by default**. Enable per-project in `.claude/settings.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"curator": {
|
|
24
|
+
"enabled": true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or globally in `~/.claude/settings.json`. See [`config.json`](config.json) for the full set of tunable defaults.
|
|
30
|
+
|
|
31
|
+
## Storage layout
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
~/.onlooker/curator/<project-key>/
|
|
35
|
+
├── manifest.json # project metadata
|
|
36
|
+
├── last_llm_sweep.json # watermark for the weekly LLM pass
|
|
37
|
+
├── last_cheap_scan.json # watermark for the per-session check
|
|
38
|
+
└── findings/<ulid>.json # one finding per file
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Status
|
|
42
|
+
|
|
43
|
+
This plugin ships **scaffolding + four cheap-tier checks (date_decayed, path_broken, broken_index, orphaned_memory) + SessionStart surfacer**. Deferred to follow-up landings:
|
|
44
|
+
|
|
45
|
+
- **LLM contradiction sweep** — design and the watermark plumbing (`last_llm_sweep.json`) are in place; the Haiku pair-evaluation loop is not implemented. `llm_sweep.enabled` defaults to `false` and is a no-op until the sweep ships.
|
|
46
|
+
- **`/curator review` interactive walkthrough** — accept / prune / edit / reclassify / acknowledge / defer for surfaced findings.
|
|
47
|
+
- **Usage tracker** (zero-recall-window findings) — depends on a substrate-level `memory.recalled` emitter that doesn't exist yet. `usage_tracker.enabled` defaults to `false`; see [`docs/design.md`](docs/design.md) Open Question #1.
|
|
48
|
+
- **Symbol reference check** — backtick-wrapped identifiers grep'd against the repo. Not yet wired.
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- The `ecosystem` plugin installed (for `~/.onlooker/` substrate).
|
|
53
|
+
- `jq` for JSON manipulation.
|
|
54
|
+
- `python3` for date math and path resolution.
|
|
55
|
+
- `git` to resolve the project key and (for the reference check) the repo root.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugin_name": "curator",
|
|
3
|
+
"storage_path": "~/.onlooker",
|
|
4
|
+
"curator": {
|
|
5
|
+
"enabled": false,
|
|
6
|
+
"memory_store_path": "${HOME}/.claude/projects/${CLAUDE_PROJECT_ENCODED}/memory",
|
|
7
|
+
"cheap_checks": {
|
|
8
|
+
"enabled": true,
|
|
9
|
+
"wall_clock_budget_ms": 500,
|
|
10
|
+
"skip_if_session_age_under_seconds": 5
|
|
11
|
+
},
|
|
12
|
+
"date_check": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"date_grace_period_days": 14
|
|
15
|
+
},
|
|
16
|
+
"reference_check": {
|
|
17
|
+
"enabled": true,
|
|
18
|
+
"check_urls": false,
|
|
19
|
+
"url_allowlist": []
|
|
20
|
+
},
|
|
21
|
+
"usage_tracker": {
|
|
22
|
+
"enabled": false,
|
|
23
|
+
"usage_window_days": 30,
|
|
24
|
+
"_note": "Disabled until the substrate memory.recalled emitter ships. Setting to true today is a no-op."
|
|
25
|
+
},
|
|
26
|
+
"llm_sweep": {
|
|
27
|
+
"enabled": false,
|
|
28
|
+
"_note": "Disabled until the Haiku pair-evaluation loop ships. Setting to true today is a no-op.",
|
|
29
|
+
"model": "claude-haiku-4-5-20251001",
|
|
30
|
+
"temperature": 0.2,
|
|
31
|
+
"max_output_tokens": 96,
|
|
32
|
+
"interval_days": 7,
|
|
33
|
+
"max_pair_evaluations_per_sweep": 50,
|
|
34
|
+
"contradiction_similarity_threshold": 0.4
|
|
35
|
+
},
|
|
36
|
+
"surfacer": {
|
|
37
|
+
"max_pointer_chars": 200,
|
|
38
|
+
"skip_when_zero": true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Curator SessionStart hook.
|
|
3
|
+
#
|
|
4
|
+
# Runs cheap-tier checks against the typed memory store and emits findings
|
|
5
|
+
# under ~/.onlooker/curator/<project-key>/findings/. Surfaces a one-line
|
|
6
|
+
# pointer to /curator review when open findings exist.
|
|
7
|
+
#
|
|
8
|
+
# Hook contract:
|
|
9
|
+
# - Always exits 0. Never blocks session start.
|
|
10
|
+
# - Emits valid hookSpecificOutput JSON even when nothing to inject.
|
|
11
|
+
# - No-ops when curator.enabled is not true.
|
|
12
|
+
# - No-ops when no git context, no memory store path, or no checks pass
|
|
13
|
+
# the rate gate.
|
|
14
|
+
#
|
|
15
|
+
# LLM contradiction sweep is deferred to a follow-up commit.
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
21
|
+
|
|
22
|
+
_ECOSYSTEM_ROOT="${ONLOOKER_ECOSYSTEM_ROOT:-}"
|
|
23
|
+
if [[ -z "$_ECOSYSTEM_ROOT" ]]; then
|
|
24
|
+
_candidate="$(cd "${PLUGIN_ROOT}/../.." 2>/dev/null && pwd)"
|
|
25
|
+
if [[ -f "${_candidate}/scripts/lib/validate-path.sh" ]]; then
|
|
26
|
+
_ECOSYSTEM_ROOT="$_candidate"
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
if [[ -n "$_ECOSYSTEM_ROOT" && -f "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh" ]]; then
|
|
30
|
+
# shellcheck disable=SC1091
|
|
31
|
+
CLAUDE_PLUGIN_ROOT="$_ECOSYSTEM_ROOT" source "${_ECOSYSTEM_ROOT}/scripts/lib/validate-path.sh"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# shellcheck source=../lib/curator-config.sh
|
|
35
|
+
source "${PLUGIN_ROOT}/scripts/lib/curator-config.sh"
|
|
36
|
+
# shellcheck source=../lib/curator-project-key.sh
|
|
37
|
+
source "${PLUGIN_ROOT}/scripts/lib/curator-project-key.sh"
|
|
38
|
+
# shellcheck source=../lib/curator-ulid.sh
|
|
39
|
+
source "${PLUGIN_ROOT}/scripts/lib/curator-ulid.sh"
|
|
40
|
+
# shellcheck source=../lib/curator-storage.sh
|
|
41
|
+
source "${PLUGIN_ROOT}/scripts/lib/curator-storage.sh"
|
|
42
|
+
# shellcheck source=../lib/curator-emit.sh
|
|
43
|
+
source "${PLUGIN_ROOT}/scripts/lib/curator-emit.sh"
|
|
44
|
+
# shellcheck source=../lib/curator-memory-reader.sh
|
|
45
|
+
source "${PLUGIN_ROOT}/scripts/lib/curator-memory-reader.sh"
|
|
46
|
+
# shellcheck source=../lib/curator-checks.sh
|
|
47
|
+
source "${PLUGIN_ROOT}/scripts/lib/curator-checks.sh"
|
|
48
|
+
|
|
49
|
+
_emit() {
|
|
50
|
+
local context="${1:-}"
|
|
51
|
+
jq -cn --arg ctx "$context" '{
|
|
52
|
+
hookSpecificOutput: {
|
|
53
|
+
hookEventName: "SessionStart",
|
|
54
|
+
additionalContext: $ctx
|
|
55
|
+
}
|
|
56
|
+
}'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
INPUT=$(cat 2>/dev/null || true)
|
|
60
|
+
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || CWD=""
|
|
61
|
+
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""' 2>/dev/null) || SESSION_ID=""
|
|
62
|
+
[[ -z "$CWD" ]] && CWD="$(pwd)"
|
|
63
|
+
[[ -z "$SESSION_ID" ]] && SESSION_ID="unknown"
|
|
64
|
+
|
|
65
|
+
REPO_ROOT=$(curator_project_repo_root "$CWD")
|
|
66
|
+
curator_config_load "$REPO_ROOT"
|
|
67
|
+
|
|
68
|
+
if ! curator_config_enabled; then
|
|
69
|
+
_emit ""
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
PROJECT_KEY=$(curator_project_key "$CWD")
|
|
74
|
+
if [[ -z "$PROJECT_KEY" ]]; then
|
|
75
|
+
_emit ""
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
curator_storage_init "$PROJECT_KEY" || { _emit ""; exit 0; }
|
|
80
|
+
REMOTE_URL=$(curator_project_remote_url "$CWD")
|
|
81
|
+
curator_storage_write_manifest "$PROJECT_KEY" "$REMOTE_URL" "$REPO_ROOT" || true
|
|
82
|
+
|
|
83
|
+
# ----------------------------------------------------------------------------
|
|
84
|
+
# Resolve the typed memory store path. Skip the audit if it can't be resolved.
|
|
85
|
+
# ----------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
MEM_PATH_TEMPLATE=$(curator_config_get '.curator.memory_store_path')
|
|
88
|
+
if [[ -z "$MEM_PATH_TEMPLATE" || "$MEM_PATH_TEMPLATE" == "null" ]]; then
|
|
89
|
+
MEM_PATH_TEMPLATE='${HOME}/.claude/projects/${CLAUDE_PROJECT_ENCODED}/memory'
|
|
90
|
+
fi
|
|
91
|
+
MEM_DIR=$(curator_memory_resolve_path "$MEM_PATH_TEMPLATE")
|
|
92
|
+
|
|
93
|
+
if [[ -z "$MEM_DIR" || ! -d "$MEM_DIR" ]]; then
|
|
94
|
+
# No memory store, nothing to audit. Still emit a scan event so the
|
|
95
|
+
# observability stream shows curator ran.
|
|
96
|
+
curator_emit "curator.scan.started" "$SESSION_ID" "$(jq -cn '{ mode: "cheap" }')"
|
|
97
|
+
curator_emit "curator.scan.complete" "$SESSION_ID" "$(jq -cn '{
|
|
98
|
+
mode: "cheap", outcome: "ok",
|
|
99
|
+
findings_new: 0, findings_resolved: 0, duration_ms: 0
|
|
100
|
+
}')"
|
|
101
|
+
_emit ""
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# ----------------------------------------------------------------------------
|
|
106
|
+
# Cheap-tier rate gate.
|
|
107
|
+
#
|
|
108
|
+
# Three knobs:
|
|
109
|
+
# cheap_checks.enabled global on/off for the cheap tier
|
|
110
|
+
# cheap_checks.wall_clock_budget_ms abort phases past this elapsed
|
|
111
|
+
# surfacer.max_pointer_chars truncate additionalContext at this
|
|
112
|
+
# ----------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
CHEAP_ENABLED=$(curator_config_get '.curator.cheap_checks.enabled')
|
|
115
|
+
SCAN_START_MS=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null) \
|
|
116
|
+
|| SCAN_START_MS=$(($(date +%s) * 1000))
|
|
117
|
+
SCAN_START_S=$((SCAN_START_MS / 1000))
|
|
118
|
+
|
|
119
|
+
curator_emit "curator.scan.started" "$SESSION_ID" "$(jq -cn '{ mode: "cheap" }')"
|
|
120
|
+
|
|
121
|
+
if [[ "$CHEAP_ENABLED" == "false" ]]; then
|
|
122
|
+
# Cheap tier explicitly off — emit scan.complete with skip_reason
|
|
123
|
+
# and skip straight to the surfacer (which reads previously-persisted
|
|
124
|
+
# findings, if any).
|
|
125
|
+
curator_emit "curator.scan.complete" "$SESSION_ID" "$(jq -cn \
|
|
126
|
+
--arg mode "cheap" --arg outcome "skipped" \
|
|
127
|
+
--arg skip_reason "disabled" \
|
|
128
|
+
--argjson findings_new 0 --argjson findings_resolved 0 \
|
|
129
|
+
--argjson duration_ms 0 \
|
|
130
|
+
'{ mode: $mode, outcome: $outcome, skip_reason: $skip_reason,
|
|
131
|
+
findings_new: $findings_new, findings_resolved: $findings_resolved,
|
|
132
|
+
duration_ms: $duration_ms }')"
|
|
133
|
+
FINDINGS_NEW=0
|
|
134
|
+
# Skip the per-check pipeline; fall through to the surfacer.
|
|
135
|
+
OUTCOME_FOR_SCAN_COMPLETE="skipped"
|
|
136
|
+
else
|
|
137
|
+
OUTCOME_FOR_SCAN_COMPLETE="ok"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
BUDGET_MS=$(curator_config_get '.curator.cheap_checks.wall_clock_budget_ms')
|
|
141
|
+
[[ -z "$BUDGET_MS" || "$BUDGET_MS" == "null" ]] && BUDGET_MS=500
|
|
142
|
+
|
|
143
|
+
_curator_now_ms() {
|
|
144
|
+
python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null \
|
|
145
|
+
|| echo "$(( $(date +%s) * 1000 ))"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_curator_over_budget() {
|
|
149
|
+
local now elapsed
|
|
150
|
+
now=$(_curator_now_ms)
|
|
151
|
+
elapsed=$((now - SCAN_START_MS))
|
|
152
|
+
(( elapsed > BUDGET_MS ))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# When the cheap tier is enabled, run the four checks under the budget
|
|
156
|
+
# gate. Each phase checks the budget BEFORE its work — partial phases
|
|
157
|
+
# are allowed to finish since check work itself is cheap.
|
|
158
|
+
DATE_FINDINGS='[]'
|
|
159
|
+
PATH_FINDINGS='[]'
|
|
160
|
+
BROKEN_INDEX='[]'
|
|
161
|
+
ORPHANED='[]'
|
|
162
|
+
BUDGET_TRIPPED="false"
|
|
163
|
+
MEMORIES='[]'
|
|
164
|
+
|
|
165
|
+
if [[ "$CHEAP_ENABLED" != "false" ]]; then
|
|
166
|
+
if _curator_over_budget; then
|
|
167
|
+
BUDGET_TRIPPED="true"
|
|
168
|
+
else
|
|
169
|
+
MEMORIES=$(curator_memory_load_all "$MEM_DIR")
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
DATE_GRACE=$(curator_config_get '.curator.date_check.date_grace_period_days')
|
|
173
|
+
[[ -z "$DATE_GRACE" || "$DATE_GRACE" == "null" ]] && DATE_GRACE=14
|
|
174
|
+
DATE_CHECK_ENABLED=$(curator_config_get '.curator.date_check.enabled')
|
|
175
|
+
|
|
176
|
+
if [[ "$BUDGET_TRIPPED" != "true" && "$DATE_CHECK_ENABLED" != "false" ]]; then
|
|
177
|
+
if _curator_over_budget; then
|
|
178
|
+
BUDGET_TRIPPED="true"
|
|
179
|
+
else
|
|
180
|
+
DATE_FINDINGS=$(curator_check_dates "$MEMORIES" "$DATE_GRACE") || DATE_FINDINGS='[]'
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
REF_CHECK_ENABLED=$(curator_config_get '.curator.reference_check.enabled')
|
|
185
|
+
if [[ "$BUDGET_TRIPPED" != "true" && "$REF_CHECK_ENABLED" != "false" && -n "$REPO_ROOT" ]]; then
|
|
186
|
+
if _curator_over_budget; then
|
|
187
|
+
BUDGET_TRIPPED="true"
|
|
188
|
+
else
|
|
189
|
+
PATH_FINDINGS=$(curator_check_paths "$MEMORIES" "$REPO_ROOT") || PATH_FINDINGS='[]'
|
|
190
|
+
fi
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
if [[ "$BUDGET_TRIPPED" != "true" ]]; then
|
|
194
|
+
if _curator_over_budget; then
|
|
195
|
+
BUDGET_TRIPPED="true"
|
|
196
|
+
else
|
|
197
|
+
BROKEN_INDEX=$(curator_check_broken_index "$MEMORIES")
|
|
198
|
+
ORPHANED=$(curator_check_orphaned "$MEMORIES")
|
|
199
|
+
fi
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# ----------------------------------------------------------------------------
|
|
204
|
+
# Persist findings (with dedup by deduped_hash) and emit per-finding events.
|
|
205
|
+
# Skipped entirely when the cheap tier is disabled — the disabled path above
|
|
206
|
+
# already emitted scan.complete and set FINDINGS_NEW=0.
|
|
207
|
+
# ----------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
NOW_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
210
|
+
[[ "$CHEAP_ENABLED" == "false" ]] || FINDINGS_NEW=0
|
|
211
|
+
|
|
212
|
+
_write_finding() {
|
|
213
|
+
local kind="$1"
|
|
214
|
+
local payload="$2"
|
|
215
|
+
local hash_input
|
|
216
|
+
hash_input="${kind}|$(printf '%s' "$payload" | jq -cS '.')"
|
|
217
|
+
local hash
|
|
218
|
+
hash=$(curator_finding_hash "$hash_input") || hash=""
|
|
219
|
+
[[ -z "$hash" ]] && return 0
|
|
220
|
+
|
|
221
|
+
# Dedup: skip if an open finding with the same hash already exists.
|
|
222
|
+
if curator_storage_has_finding_with_hash "$PROJECT_KEY" "$hash"; then
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
local id record
|
|
227
|
+
id=$(curator_ulid)
|
|
228
|
+
record=$(jq -n \
|
|
229
|
+
--arg id "$id" \
|
|
230
|
+
--arg kind "$kind" \
|
|
231
|
+
--arg created_at "$NOW_TS" \
|
|
232
|
+
--arg deduped_hash "$hash" \
|
|
233
|
+
--argjson detail "$payload" \
|
|
234
|
+
'{
|
|
235
|
+
id: $id, kind: $kind, created_at: $created_at,
|
|
236
|
+
status: "open", deduped_hash: $deduped_hash, detail: $detail
|
|
237
|
+
}')
|
|
238
|
+
curator_storage_write_finding "$PROJECT_KEY" "$id" "$record" >/dev/null || return 0
|
|
239
|
+
FINDINGS_NEW=$((FINDINGS_NEW + 1))
|
|
240
|
+
|
|
241
|
+
# Per-kind event payload.
|
|
242
|
+
local event_type event_payload
|
|
243
|
+
event_type="curator.finding.${kind}"
|
|
244
|
+
event_payload=$(jq -cn --arg fid "$id" --argjson detail "$payload" \
|
|
245
|
+
'{ finding_id: $fid } + $detail')
|
|
246
|
+
curator_emit "$event_type" "$SESSION_ID" "$event_payload"
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Convert each finding-array entry into a stored + emitted finding.
|
|
250
|
+
_emit_kind_findings() {
|
|
251
|
+
local kind="$1" findings_json="$2"
|
|
252
|
+
local n
|
|
253
|
+
n=$(printf '%s' "$findings_json" | jq 'length' 2>/dev/null) || n=0
|
|
254
|
+
local i payload
|
|
255
|
+
for ((i = 0; i < n; i++)); do
|
|
256
|
+
payload=$(printf '%s' "$findings_json" | jq -c ".[$i]")
|
|
257
|
+
[[ -z "$payload" || "$payload" == "null" ]] && continue
|
|
258
|
+
_write_finding "$kind" "$payload"
|
|
259
|
+
done
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if [[ "$CHEAP_ENABLED" != "false" ]]; then
|
|
263
|
+
_emit_kind_findings "date_decayed" "$DATE_FINDINGS"
|
|
264
|
+
_emit_kind_findings "path_broken" "$PATH_FINDINGS"
|
|
265
|
+
_emit_kind_findings "broken_index" "$BROKEN_INDEX"
|
|
266
|
+
_emit_kind_findings "orphaned_memory" "$ORPHANED"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# ----------------------------------------------------------------------------
|
|
270
|
+
# Watermark + scan.complete. The disabled-tier branch above already emitted
|
|
271
|
+
# scan.complete; this branch fires only when the cheap tier ran (success or
|
|
272
|
+
# budget tripped).
|
|
273
|
+
# ----------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
if [[ "$CHEAP_ENABLED" != "false" ]]; then
|
|
276
|
+
curator_storage_write_watermark "$(curator_last_cheap_scan_path "$PROJECT_KEY")" || true
|
|
277
|
+
|
|
278
|
+
DURATION_MS=$(( $(_curator_now_ms) - SCAN_START_MS ))
|
|
279
|
+
if [[ "$BUDGET_TRIPPED" == "true" ]]; then
|
|
280
|
+
curator_emit "curator.scan.complete" "$SESSION_ID" "$(jq -cn \
|
|
281
|
+
--arg mode "cheap" --arg outcome "skipped" \
|
|
282
|
+
--arg skip_reason "over_budget" \
|
|
283
|
+
--argjson findings_new "$FINDINGS_NEW" \
|
|
284
|
+
--argjson findings_resolved 0 \
|
|
285
|
+
--argjson duration_ms "$DURATION_MS" \
|
|
286
|
+
'{ mode: $mode, outcome: $outcome, skip_reason: $skip_reason,
|
|
287
|
+
findings_new: $findings_new,
|
|
288
|
+
findings_resolved: $findings_resolved,
|
|
289
|
+
duration_ms: $duration_ms }')"
|
|
290
|
+
else
|
|
291
|
+
curator_emit "curator.scan.complete" "$SESSION_ID" "$(jq -cn \
|
|
292
|
+
--arg mode "cheap" --arg outcome "ok" \
|
|
293
|
+
--argjson findings_new "$FINDINGS_NEW" \
|
|
294
|
+
--argjson findings_resolved 0 \
|
|
295
|
+
--argjson duration_ms "$DURATION_MS" \
|
|
296
|
+
'{ mode: $mode, outcome: $outcome,
|
|
297
|
+
findings_new: $findings_new,
|
|
298
|
+
findings_resolved: $findings_resolved,
|
|
299
|
+
duration_ms: $duration_ms }')"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# ----------------------------------------------------------------------------
|
|
304
|
+
# Surfacer.
|
|
305
|
+
# ----------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
SKIP_WHEN_ZERO=$(curator_config_get '.curator.surfacer.skip_when_zero')
|
|
308
|
+
[[ -z "$SKIP_WHEN_ZERO" || "$SKIP_WHEN_ZERO" == "null" ]] && SKIP_WHEN_ZERO="true"
|
|
309
|
+
|
|
310
|
+
OPEN_COUNT=$(curator_storage_count_open "$PROJECT_KEY")
|
|
311
|
+
[[ -z "$OPEN_COUNT" || "$OPEN_COUNT" == "null" ]] && OPEN_COUNT=0
|
|
312
|
+
|
|
313
|
+
if [[ "$OPEN_COUNT" -eq 0 && "$SKIP_WHEN_ZERO" == "true" ]]; then
|
|
314
|
+
_emit ""
|
|
315
|
+
exit 0
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Build a compact "2 path-broken, 1 date-decayed" descriptor for the
|
|
319
|
+
# pointer message.
|
|
320
|
+
COUNTS_BY_KIND=$(curator_storage_open_counts_by_kind "$PROJECT_KEY")
|
|
321
|
+
SUMMARY=$(printf '%s' "$COUNTS_BY_KIND" | jq -r '
|
|
322
|
+
map( (.count|tostring) + " " + (.kind | gsub("_"; "-")) )
|
|
323
|
+
| join(", ")
|
|
324
|
+
')
|
|
325
|
+
|
|
326
|
+
CONTEXT=$(printf 'Curator: %s open finding%s (%s). Review with `/curator review`.' \
|
|
327
|
+
"$OPEN_COUNT" \
|
|
328
|
+
"$([ "$OPEN_COUNT" -eq 1 ] && echo "" || echo "s")" \
|
|
329
|
+
"$SUMMARY")
|
|
330
|
+
|
|
331
|
+
# Cap the pointer length so a long per-kind summary never overflows the
|
|
332
|
+
# user's SessionStart context.
|
|
333
|
+
MAX_POINTER=$(curator_config_get '.curator.surfacer.max_pointer_chars')
|
|
334
|
+
[[ -z "$MAX_POINTER" || "$MAX_POINTER" == "null" ]] && MAX_POINTER=200
|
|
335
|
+
if [[ "${#CONTEXT}" -gt "$MAX_POINTER" ]]; then
|
|
336
|
+
# Reserve room for the truncation ellipsis without exceeding the cap.
|
|
337
|
+
TRUNC=$((MAX_POINTER - 1))
|
|
338
|
+
(( TRUNC < 1 )) && TRUNC=1
|
|
339
|
+
CONTEXT="${CONTEXT:0:TRUNC}…"
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
_emit "$CONTEXT"
|
|
343
|
+
exit 0
|