@onlooker-community/ecosystem 0.10.0 → 0.15.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 +39 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.github/copilot-instructions.md +46 -0
- package/.github/workflows/coverage.yml +78 -0
- package/.github/workflows/release.yml +24 -8
- package/.github/workflows/test.yml +3 -0
- package/.markdownlintignore +3 -0
- package/.release-please-manifest.json +5 -1
- package/CHANGELOG.md +44 -0
- package/README.md +58 -13
- package/config.json +6 -1
- package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
- package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
- package/docs/adr/003-ulid-over-uuid.md +40 -0
- package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
- package/docs/architecture.md +123 -0
- package/hooks/hooks.json +4 -0
- package/package.json +13 -7
- package/plugins/archivist/.claude-plugin/plugin.json +14 -0
- package/plugins/archivist/CHANGELOG.md +8 -0
- package/plugins/archivist/README.md +105 -0
- package/plugins/archivist/config.json +18 -0
- package/plugins/archivist/hooks/hooks.json +35 -0
- package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
- package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
- package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
- package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
- package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
- package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
- package/plugins/cartographer/.claude-plugin/plugin.json +14 -0
- package/plugins/cartographer/CHANGELOG.md +27 -0
- package/plugins/cartographer/README.md +113 -0
- package/plugins/cartographer/config.json +21 -0
- package/plugins/cartographer/docs/adr/001-background-audit-launch.md +28 -0
- package/plugins/cartographer/docs/adr/002-flock-pid-file-fallback.md +30 -0
- package/plugins/cartographer/docs/adr/003-at-least-once-event-delivery.md +32 -0
- package/plugins/cartographer/docs/adr/004-exclude-paths-replace-semantics.md +27 -0
- package/plugins/cartographer/hooks/hooks.json +44 -0
- package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +87 -0
- package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +89 -0
- package/plugins/cartographer/scripts/lib/cartographer-analyze.sh +286 -0
- package/plugins/cartographer/scripts/lib/cartographer-collect.sh +59 -0
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +105 -0
- package/plugins/cartographer/scripts/lib/cartographer-events.sh +82 -0
- package/plugins/cartographer/scripts/lib/cartographer-lock.sh +38 -0
- package/plugins/cartographer/scripts/lib/cartographer-project-key.sh +55 -0
- package/plugins/cartographer/scripts/lib/cartographer-ulid.sh +47 -0
- package/plugins/cartographer/scripts/run-audit.sh +309 -0
- package/plugins/cartographer/skills/cartographer/SKILL.md +154 -0
- package/plugins/echo/.claude-plugin/plugin.json +14 -0
- package/plugins/echo/CHANGELOG.md +24 -0
- package/plugins/echo/README.md +110 -0
- package/plugins/echo/config.json +15 -0
- package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
- package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
- package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
- package/plugins/echo/hooks/hooks.json +15 -0
- package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
- package/plugins/echo/scripts/lib/echo-config.sh +108 -0
- package/plugins/echo/scripts/lib/echo-events.sh +74 -0
- package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
- package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
- package/plugins/tribunal/CHANGELOG.md +10 -0
- package/plugins/tribunal/README.md +134 -0
- package/plugins/tribunal/agents/tribunal-actor.md +35 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
- package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
- package/plugins/tribunal/config.json +50 -0
- package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
- package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
- package/plugins/tribunal/hooks/hooks.json +15 -0
- package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
- package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
- package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
- package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
- package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
- package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
- package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
- package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
- package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
- package/release-please-config.json +59 -5
- package/scripts/coverage/bash-coverage.mjs +169 -0
- package/scripts/coverage/format-comment.mjs +120 -0
- package/scripts/coverage/run-coverage.mjs +151 -0
- package/scripts/hooks/agent-spawn-tracker.sh +4 -4
- package/scripts/hooks/prompt-rule-injector.sh +122 -0
- package/scripts/lib/portable-lock.sh +48 -0
- package/scripts/lib/prompt-rules.sh +207 -0
- package/scripts/lib/tool-history.sh +7 -8
- package/scripts/lib/validate-path.sh +4 -0
- package/scripts/lint/check-manifests.mjs +314 -0
- package/scripts/lint/check-references.mjs +311 -0
- package/skills/list-prompt-rules/SKILL.md +15 -0
- package/test/bats/archivist-config-files.bats +60 -0
- package/test/bats/archivist-config.bats +54 -0
- package/test/bats/archivist-inject.bats +73 -0
- package/test/bats/archivist-project-key.bats +75 -0
- package/test/bats/archivist-storage.bats +119 -0
- package/test/bats/archivist-ulid.bats +36 -0
- package/test/bats/cartographer-config.bats +107 -0
- package/test/bats/cartographer-lock.bats +77 -0
- package/test/bats/cartographer-ulid.bats +56 -0
- package/test/bats/config.bats +10 -10
- package/test/bats/echo-config.bats +90 -0
- package/test/bats/echo-events.bats +121 -0
- package/test/bats/echo-project-key.bats +115 -0
- package/test/bats/echo-stop-hook.bats +101 -0
- package/test/bats/echo-ulid.bats +38 -0
- package/test/bats/portable-lock.bats +62 -0
- package/test/bats/prompt-rules.bats +269 -0
- package/test/bats/tribunal-aggregate.bats +77 -0
- package/test/bats/tribunal-config.bats +86 -0
- package/test/bats/tribunal-events.bats +209 -0
- package/test/bats/tribunal-gate.bats +95 -0
- package/test/bats/tribunal-jury.bats +80 -0
- package/test/bats/tribunal-rubric.bats +119 -0
- package/test/bats/tribunal-stop-hook.bats +73 -0
- package/test/bats/tribunal-verdict.bats +71 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
- package/test/helpers/setup.bash +9 -0
- package/test/node/check-manifests.test.mjs +173 -0
- package/test/node/check-references.test.mjs +279 -0
- package/test/node/coverage.test.mjs +143 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Cartographer
|
|
2
|
+
|
|
3
|
+
Proactive, periodic auditor of the persistent instruction layer shaping every Claude Code session.
|
|
4
|
+
|
|
5
|
+
Cartographer discovers all `CLAUDE.md`, `AGENTS.md`, and `.claude/rules/` files in your project, builds a semantic map of their relationships, and surfaces contradictions, stale references, dead rules, and scope collisions — before they cause expensive agent misbehavior.
|
|
6
|
+
|
|
7
|
+
Every other Onlooker plugin is reactive. Cartographer is the exception.
|
|
8
|
+
|
|
9
|
+
## What it detects
|
|
10
|
+
|
|
11
|
+
| Finding type | Description |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `contradiction` | Two rules that cannot both be satisfied simultaneously |
|
|
14
|
+
| `dead_rule` | A rule fully subsumed by a more specific rule elsewhere |
|
|
15
|
+
| `stale_ref` | A reference to a file path, tool, or command that no longer exists |
|
|
16
|
+
| `scope_collision` | A project rule that duplicates or silently overrides a global `~/.claude/CLAUDE.md` rule |
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Cartographer is part of the Onlooker ecosystem monorepo. It requires the ecosystem plugin to be installed first.
|
|
21
|
+
|
|
22
|
+
## Activation
|
|
23
|
+
|
|
24
|
+
Cartographer is **disabled by default**. Enable it per-project in `.claude/settings.json`:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"cartographer": {
|
|
29
|
+
"enabled": true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or globally in `~/.claude/settings.json` to audit all projects.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Automatic (SessionStart)
|
|
39
|
+
|
|
40
|
+
Once enabled, Cartographer audits automatically every 24 hours (configurable). The audit runs as a detached background process — your session is not blocked.
|
|
41
|
+
|
|
42
|
+
Findings appear in the next `/cartographer` invocation or in any event log consumer subscribed to `cartographer.issue.found`.
|
|
43
|
+
|
|
44
|
+
### On-demand
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
/cartographer # full audit, foreground
|
|
48
|
+
/cartographer --verbose # show all known findings (no bus events)
|
|
49
|
+
/cartographer --status # running state + last completion time
|
|
50
|
+
/cartographer --force # kill running audit and restart
|
|
51
|
+
/cartographer --phase=contradiction # single-phase audit
|
|
52
|
+
/cartographer --scope=src/ # scoped to a subdirectory
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
All options are optional. Defaults shown:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"cartographer": {
|
|
62
|
+
"enabled": false,
|
|
63
|
+
"audit_interval_hours": 24,
|
|
64
|
+
"phase_timeout_seconds": 60,
|
|
65
|
+
"total_timeout_seconds": 600,
|
|
66
|
+
"extraction": {
|
|
67
|
+
"model": "claude-haiku-4-5-20251001",
|
|
68
|
+
"max_output_tokens": 2048
|
|
69
|
+
},
|
|
70
|
+
"synthesis": {
|
|
71
|
+
"model": "claude-haiku-4-5-20251001",
|
|
72
|
+
"max_output_tokens": 2048
|
|
73
|
+
},
|
|
74
|
+
"exclude_paths": [
|
|
75
|
+
"node_modules", ".git", "vendor", ".venv",
|
|
76
|
+
"dist", ".next", ".nuxt", "build", "__pycache__"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Note:** Overriding `exclude_paths` replaces the entire list. Repeat the defaults plus your additions if you want to extend rather than replace.
|
|
83
|
+
|
|
84
|
+
## Privacy
|
|
85
|
+
|
|
86
|
+
- All analysis uses `claude -p` via your existing Claude Code session — no separate API key, no new data recipient.
|
|
87
|
+
- Findings are stored only in `~/.onlooker/cartographer/<project-key>/` on your local machine.
|
|
88
|
+
- The event log (`~/.onlooker/logs/onlooker-events.jsonl`) contains finding excerpts (capped in the payload) but never full file contents.
|
|
89
|
+
|
|
90
|
+
## Storage
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
~/.onlooker/cartographer/<project-key>/
|
|
94
|
+
├── last_audit_at # unix epoch of last completed audit
|
|
95
|
+
├── audit.lock # flock target or PID file
|
|
96
|
+
├── audit.log # background audit stdout/stderr
|
|
97
|
+
├── extracts/ # per-file content hash cache
|
|
98
|
+
├── findings/ # one JSON file per unique finding (atomic writes)
|
|
99
|
+
└── dedup/ # empty sentinel per emitted finding hash
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Event delivery
|
|
103
|
+
|
|
104
|
+
`cartographer.issue.found` events are delivered at-least-once. If the audit process crashes between emitting an event and writing the dedup sentinel, the finding is re-emitted once on the next run. Downstream consumers must deduplicate on `payload.finding_hash`.
|
|
105
|
+
|
|
106
|
+
## Non-goals
|
|
107
|
+
|
|
108
|
+
Cartographer will not:
|
|
109
|
+
- Modify any instruction file
|
|
110
|
+
- Block Write or Edit tool calls
|
|
111
|
+
- Enforce rule priority or style
|
|
112
|
+
- Operate across machines (findings are local)
|
|
113
|
+
- Replace human review of instruction files
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugin_name": "cartographer",
|
|
3
|
+
"storage_path": "~/.onlooker",
|
|
4
|
+
"cartographer": {
|
|
5
|
+
"enabled": false,
|
|
6
|
+
"audit_interval_hours": 24,
|
|
7
|
+
"phase_timeout_seconds": 60,
|
|
8
|
+
"total_timeout_seconds": 600,
|
|
9
|
+
"extraction": {
|
|
10
|
+
"model": "claude-haiku-4-5-20251001",
|
|
11
|
+
"max_output_tokens": 2048,
|
|
12
|
+
"chunk_size_files": 10,
|
|
13
|
+
"chunk_overlap_pct": 10
|
|
14
|
+
},
|
|
15
|
+
"synthesis": {
|
|
16
|
+
"model": "claude-haiku-4-5-20251001",
|
|
17
|
+
"max_output_tokens": 2048
|
|
18
|
+
},
|
|
19
|
+
"exclude_paths": ["node_modules", ".git", "vendor", ".venv", "dist", ".next", ".nuxt", "build", "__pycache__"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ADR-001: Background Audit Launch via nohup+setsid
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Claude Code SessionStart hooks run synchronously and block session readiness until they exit. The Cartographer audit pipeline makes multiple `claude -p` calls and can take 1–10 minutes on large repos. Blocking the session for that duration is unacceptable.
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
|
|
11
|
+
The SessionStart hook (`cartographer-session-start.sh`) performs only three fast operations before returning:
|
|
12
|
+
1. Read `last_audit_at` from a single JSON field.
|
|
13
|
+
2. Acquire a non-blocking lock (`flock -n` or PID-file fallback).
|
|
14
|
+
3. Launch `run-audit.sh` via `nohup setsid ... &`, then write the child PID and exit.
|
|
15
|
+
|
|
16
|
+
`nohup` prevents SIGHUP from reaching the child when the hook's process group is reaped. `setsid` creates a new session so the child is not in the hook's process group at all. Together they ensure the audit survives the hook exit.
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
|
|
20
|
+
- The hook returns in under 2 seconds regardless of repo size.
|
|
21
|
+
- Audit progress is visible only in `~/.onlooker/cartographer/<key>/audit.log`.
|
|
22
|
+
- The user has no immediate signal that an audit started; findings surface at the next `/cartographer` invocation or via event log consumers.
|
|
23
|
+
|
|
24
|
+
## Alternatives Considered
|
|
25
|
+
|
|
26
|
+
- **Synchronous with short timeout:** Limits audit depth; users would see partial results inconsistently.
|
|
27
|
+
- **Cron/launchd/systemd timer:** Requires out-of-process scheduler setup; breaks the "install the plugin, no other config required" contract.
|
|
28
|
+
- **PreCompact hook (like Archivist):** Archivist uses PreCompact because compaction is a natural checkpoint. Cartographer is file-change driven, not conversation-length driven — SessionStart is the better trigger.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ADR-002: flock with PID-File Fallback for Cross-Session Locking
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Multiple Claude Code sessions can open in the same project directory simultaneously (multiple terminal windows, split panes, worktrees pointing to the same project key). Without coordination, concurrent sessions can each decide an audit is due and spawn competing `claude -p` subprocesses that race on `findings/<hash>.json` writes and `last_audit_at`.
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
|
|
11
|
+
`cartographer-lock.sh` implements a two-tier lock:
|
|
12
|
+
|
|
13
|
+
1. **`flock --nonblock`** on `audit.lock` — kernel-level, atomic, preferred. Available on Linux without extra tooling.
|
|
14
|
+
2. **PID-file fallback** — reads PID from `audit.lock`, checks with `kill -0`. Used on macOS where `flock` requires `brew install flock` (coreutils) and may not be present.
|
|
15
|
+
|
|
16
|
+
Both tiers use non-blocking acquisition: the second session exits cleanly rather than queuing. The `/cartographer --force` flag kills the existing audit PID before acquiring the lock for manual override.
|
|
17
|
+
|
|
18
|
+
**Known limitation:** The PID-file fallback has a TOCTOU window between `kill -0` and writing the new PID. On single-machine developer workstations this is an acceptable risk; we are protecting against simultaneous sessions, not adversarial concurrent writes. This is documented in CLAUDE.md.
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
|
|
22
|
+
- Most Linux users get atomic kernel-level locking.
|
|
23
|
+
- macOS users without coreutils get a best-effort fallback with documented limitations.
|
|
24
|
+
- No mandatory external dependency beyond a standard shell.
|
|
25
|
+
|
|
26
|
+
## Alternatives Considered
|
|
27
|
+
|
|
28
|
+
- **Require `flock` as a mise dep:** Adds a dependency users must install; breaks the zero-config install story.
|
|
29
|
+
- **Use a socket/lockfile via `mkdir` (atomic on POSIX):** `mkdir` creates atomically; the lock is the directory existence. More portable than PID files but does not provide stale-lock recovery.
|
|
30
|
+
- **Accept duplicate concurrent audits:** Duplicate findings are eventually deduplicated by hash; correctness is preserved but wastes resources.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ADR-003: At-Least-Once Event Delivery with finding_hash Deduplication
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Cartographer emits `cartographer.issue.found` events for new findings, then writes a sentinel file to `dedup/<hash>` to mark them as known. If the process crashes between the event emission and the sentinel write, the same finding is re-emitted on the next audit run.
|
|
8
|
+
|
|
9
|
+
We must choose between:
|
|
10
|
+
- **Exactly-once:** write sentinel first, then emit event. If the process crashes after writing the sentinel but before emitting, the event is permanently lost.
|
|
11
|
+
- **At-least-once:** emit event first, then write sentinel. If the process crashes between, the event is re-emitted once on the next run.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Use **at-least-once delivery**. Findings carry a `finding_hash` in their event payload. Downstream consumers (event log aggregators, Linear integrations, dashboards) must deduplicate on `payload.finding_hash` in their own stores.
|
|
16
|
+
|
|
17
|
+
**Rationale:** A missed finding (exactly-once failure) silently suppresses a real issue. A duplicate finding (at-least-once failure) is visible and correctable. For an advisory tool, false negatives are worse than false positives.
|
|
18
|
+
|
|
19
|
+
**At-most-once per process run** is still guaranteed: within a single `run-audit.sh` invocation, the dedup sentinel is checked before emitting, so the same finding is emitted at most once per run.
|
|
20
|
+
|
|
21
|
+
The delivery contract is documented in SKILL.md and the plugin CLAUDE.md.
|
|
22
|
+
|
|
23
|
+
## Consequences
|
|
24
|
+
|
|
25
|
+
- Downstream consumers must implement `finding_hash`-keyed deduplication.
|
|
26
|
+
- A crash during `run_emit` will re-emit at most N findings on the next run (where N is the number of new findings after the crash point).
|
|
27
|
+
- The behavior is deterministic: re-emissions carry the same `finding_hash` as the original, so dedup is always possible.
|
|
28
|
+
|
|
29
|
+
## Alternatives Considered
|
|
30
|
+
|
|
31
|
+
- **Write-then-emit (exactly-once attempt):** Simpler logic, but loses findings on crash. Unacceptable for an advisory auditor.
|
|
32
|
+
- **Two-phase commit (journal):** Write an intent log, emit, mark complete. Correct but complex for a shell script; deferred to v0.2 if needed.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ADR-004: exclude_paths Uses Replace Semantics
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
The `exclude_paths` config key lists directory name substrings that `find` should skip. Users may want to customize this list — adding project-specific directories like `fixtures/` or `testdata/`. Two approaches:
|
|
8
|
+
|
|
9
|
+
1. **Replace semantics:** Overriding `exclude_paths` replaces the entire list. Users who want to extend must repeat the defaults plus their additions.
|
|
10
|
+
2. **Append semantics via `exclude_paths_extra`:** A separate key for user additions; the defaults are always present.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Use **replace semantics** for v0.1. The default list (`node_modules`, `.git`, `vendor`, `.venv`, `dist`, `.next`, `.nuxt`, `build`, `__pycache__`) is shipped in `config.json`. When a user overrides `exclude_paths` in `.claude/settings.json`, they replace the entire list.
|
|
15
|
+
|
|
16
|
+
**Rationale:** The layered config merge (`jq * merge`) already uses replace semantics for arrays — this is consistent with how other plugin config arrays work (e.g., tribunal's `judge_types`). Introducing a separate `exclude_paths_extra` key adds surface area without clear demand in v0.1.
|
|
17
|
+
|
|
18
|
+
**Documented limitation:** Users who override `exclude_paths` and forget to include `node_modules` or `.git` will audit those directories. This is documented in the configuration reference.
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
|
|
22
|
+
- Config behavior is consistent with other plugin array keys.
|
|
23
|
+
- Users who want to extend must copy-paste the defaults plus their additions. This is mildly annoying but rare.
|
|
24
|
+
|
|
25
|
+
## Future Option
|
|
26
|
+
|
|
27
|
+
If users consistently request append behavior, `exclude_paths_extra` (a supplementary array that merges with the defaults) can be added in v0.2 without breaking existing configs.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/cartographer-session-start.sh"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Write",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/cartographer-post-write.sh"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Edit",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/cartographer-post-write.sh"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"matcher": "MultiEdit",
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/cartographer-post-write.sh"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cartographer-post-write.sh — PostToolUse hook for Write / Edit / MultiEdit.
|
|
3
|
+
#
|
|
4
|
+
# Triggers a targeted single-file re-audit when a CLAUDE.md file is modified.
|
|
5
|
+
# Uses exact basename matching — editor swap files are excluded by definition.
|
|
6
|
+
# Always exits 0 (never blocks the tool call).
|
|
7
|
+
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
|
|
10
|
+
# Recursion guard — prevents a claude -p subprocess spawned by run-audit.sh
|
|
11
|
+
# from re-triggering this hook.
|
|
12
|
+
[[ "${CARTOGRAPHER_NESTED:-0}" == "1" ]] && exit 0
|
|
13
|
+
export CARTOGRAPHER_NESTED=1
|
|
14
|
+
|
|
15
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
16
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
17
|
+
|
|
18
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-config.sh"
|
|
19
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-project-key.sh"
|
|
20
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-lock.sh"
|
|
21
|
+
|
|
22
|
+
HOOK_INPUT=$(cat)
|
|
23
|
+
CWD=$(printf '%s' "$HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
24
|
+
_HOOK_SESSION_ID=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
25
|
+
export _HOOK_SESSION_ID
|
|
26
|
+
|
|
27
|
+
[[ -z "$CWD" ]] && exit 0
|
|
28
|
+
|
|
29
|
+
# Extract the written file path from tool input
|
|
30
|
+
TOOL_TARGET=$(printf '%s' "$HOOK_INPUT" \
|
|
31
|
+
| jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
|
|
32
|
+
[[ -z "$TOOL_TARGET" ]] && exit 0
|
|
33
|
+
|
|
34
|
+
# Canonicalize the path (resolve symlinks where possible)
|
|
35
|
+
if command -v realpath &>/dev/null; then
|
|
36
|
+
CANONICAL=$(realpath "$TOOL_TARGET" 2>/dev/null) || CANONICAL="$TOOL_TARGET"
|
|
37
|
+
elif command -v readlink &>/dev/null; then
|
|
38
|
+
CANONICAL=$(readlink -f "$TOOL_TARGET" 2>/dev/null) || CANONICAL="$TOOL_TARGET"
|
|
39
|
+
else
|
|
40
|
+
CANONICAL="$TOOL_TARGET"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Exact basename match — swap files (.swp, ~, .#) are excluded by this check
|
|
44
|
+
TARGET_BASENAME=$(basename "$CANONICAL")
|
|
45
|
+
[[ "$TARGET_BASENAME" != "CLAUDE.md" ]] && exit 0
|
|
46
|
+
|
|
47
|
+
REPO_ROOT=$(cartographer_project_repo_root "$CWD")
|
|
48
|
+
cartographer_config_load "$REPO_ROOT"
|
|
49
|
+
|
|
50
|
+
cartographer_config_enabled || exit 0
|
|
51
|
+
|
|
52
|
+
PROJECT_KEY=$(cartographer_project_key "$CWD")
|
|
53
|
+
[[ -z "$PROJECT_KEY" ]] && exit 0
|
|
54
|
+
|
|
55
|
+
ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}"
|
|
56
|
+
CARTOGRAPHER_DIR="$ONLOOKER_DIR/cartographer/$PROJECT_KEY"
|
|
57
|
+
mkdir -p "$CARTOGRAPHER_DIR"
|
|
58
|
+
|
|
59
|
+
LOCK_FILE="$CARTOGRAPHER_DIR/audit.lock"
|
|
60
|
+
|
|
61
|
+
# Non-blocking lock — if a full scheduled audit is running, skip.
|
|
62
|
+
# portable-lock.sh uses atomic mkdir so no fd lifetime concerns.
|
|
63
|
+
cartographer_lock_acquire "$LOCK_FILE" || exit 0
|
|
64
|
+
|
|
65
|
+
export CARTOGRAPHER_DIR
|
|
66
|
+
export CARTOGRAPHER_TRIGGER="post_tool_use"
|
|
67
|
+
export CARTOGRAPHER_TARGET_FILE="$CANONICAL"
|
|
68
|
+
export CARTOGRAPHER_REPO_ROOT="$REPO_ROOT"
|
|
69
|
+
export ONLOOKER_DIR
|
|
70
|
+
|
|
71
|
+
if command -v setsid &>/dev/null; then
|
|
72
|
+
nohup setsid bash -c "
|
|
73
|
+
trap 'source \"$PLUGIN_ROOT/scripts/lib/cartographer-lock.sh\"; cartographer_lock_release \"$LOCK_FILE\"' EXIT
|
|
74
|
+
source \"$PLUGIN_ROOT/scripts/lib/cartographer-config.sh\"
|
|
75
|
+
cartographer_config_load \"$REPO_ROOT\"
|
|
76
|
+
exec \"$PLUGIN_ROOT/scripts/run-audit.sh\"
|
|
77
|
+
" >>"$CARTOGRAPHER_DIR/audit.log" 2>&1 &
|
|
78
|
+
else
|
|
79
|
+
nohup bash -c "
|
|
80
|
+
trap 'source \"$PLUGIN_ROOT/scripts/lib/cartographer-lock.sh\"; cartographer_lock_release \"$LOCK_FILE\"' EXIT
|
|
81
|
+
source \"$PLUGIN_ROOT/scripts/lib/cartographer-config.sh\"
|
|
82
|
+
cartographer_config_load \"$REPO_ROOT\"
|
|
83
|
+
exec \"$PLUGIN_ROOT/scripts/run-audit.sh\"
|
|
84
|
+
" >>"$CARTOGRAPHER_DIR/audit.log" 2>&1 &
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
exit 0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cartographer-session-start.sh — SessionStart hook.
|
|
3
|
+
#
|
|
4
|
+
# Fast path: reads one JSON field, acquires a lock, and launches the audit
|
|
5
|
+
# pipeline as a detached background process. Returns in under 2 seconds.
|
|
6
|
+
#
|
|
7
|
+
# Invariant: this script NEVER calls claude -p or traverses the filesystem.
|
|
8
|
+
# All heavy work runs in run-audit.sh as an orphaned child.
|
|
9
|
+
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
|
|
12
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
13
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
14
|
+
|
|
15
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-config.sh"
|
|
16
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-project-key.sh"
|
|
17
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-lock.sh"
|
|
18
|
+
|
|
19
|
+
# Parse hook input
|
|
20
|
+
HOOK_INPUT=$(cat)
|
|
21
|
+
CWD=$(printf '%s' "$HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
22
|
+
_HOOK_SESSION_ID=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // empty' 2>/dev/null)
|
|
23
|
+
export _HOOK_SESSION_ID
|
|
24
|
+
|
|
25
|
+
[[ -z "$CWD" ]] && exit 0
|
|
26
|
+
|
|
27
|
+
REPO_ROOT=$(cartographer_project_repo_root "$CWD")
|
|
28
|
+
cartographer_config_load "$REPO_ROOT"
|
|
29
|
+
|
|
30
|
+
cartographer_config_enabled || exit 0
|
|
31
|
+
|
|
32
|
+
PROJECT_KEY=$(cartographer_project_key "$CWD")
|
|
33
|
+
[[ -z "$PROJECT_KEY" ]] && exit 0
|
|
34
|
+
|
|
35
|
+
ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}"
|
|
36
|
+
CARTOGRAPHER_DIR="$ONLOOKER_DIR/cartographer/$PROJECT_KEY"
|
|
37
|
+
mkdir -p "$CARTOGRAPHER_DIR"
|
|
38
|
+
|
|
39
|
+
LOCK_FILE="$CARTOGRAPHER_DIR/audit.lock"
|
|
40
|
+
STATE_FILE="$CARTOGRAPHER_DIR/last_audit_at"
|
|
41
|
+
|
|
42
|
+
# Determine if an audit is due
|
|
43
|
+
INTERVAL_HOURS=$(cartographer_config_audit_interval_hours)
|
|
44
|
+
FIRST_RUN_TRIGGER="session_start_first_run"
|
|
45
|
+
INTERVAL_TRIGGER="session_start_interval"
|
|
46
|
+
|
|
47
|
+
if [[ ! -f "$STATE_FILE" ]]; then
|
|
48
|
+
TRIGGER="$FIRST_RUN_TRIGGER"
|
|
49
|
+
elif [[ -f "$STATE_FILE" ]]; then
|
|
50
|
+
LAST=$(cat "$STATE_FILE" 2>/dev/null || printf '0')
|
|
51
|
+
NOW=$(date +%s)
|
|
52
|
+
ELAPSED=$(( NOW - LAST ))
|
|
53
|
+
THRESHOLD=$(( INTERVAL_HOURS * 3600 ))
|
|
54
|
+
if [[ "$ELAPSED" -lt "$THRESHOLD" ]]; then
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
TRIGGER="$INTERVAL_TRIGGER"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Acquire lock non-blocking — skip if another session's audit is running.
|
|
61
|
+
# portable-lock.sh uses atomic mkdir so no fd lifetime concerns.
|
|
62
|
+
cartographer_lock_acquire "$LOCK_FILE" || exit 0
|
|
63
|
+
|
|
64
|
+
# Launch the audit detached — hook must return immediately.
|
|
65
|
+
# setsid detaches from the controlling terminal so SIGHUP on session close
|
|
66
|
+
# does not kill the background audit (ADR-001). Falls back to nohup-only on
|
|
67
|
+
# macOS where setsid requires coreutils.
|
|
68
|
+
export CARTOGRAPHER_DIR
|
|
69
|
+
export CARTOGRAPHER_TRIGGER="$TRIGGER"
|
|
70
|
+
export CARTOGRAPHER_REPO_ROOT="$REPO_ROOT"
|
|
71
|
+
export ONLOOKER_DIR
|
|
72
|
+
|
|
73
|
+
if command -v setsid &>/dev/null; then
|
|
74
|
+
nohup setsid bash -c "
|
|
75
|
+
trap 'source \"$PLUGIN_ROOT/scripts/lib/cartographer-lock.sh\"; cartographer_lock_release \"$LOCK_FILE\"' EXIT
|
|
76
|
+
source \"$PLUGIN_ROOT/scripts/lib/cartographer-config.sh\"
|
|
77
|
+
cartographer_config_load \"$REPO_ROOT\"
|
|
78
|
+
exec \"$PLUGIN_ROOT/scripts/run-audit.sh\"
|
|
79
|
+
" >>"$CARTOGRAPHER_DIR/audit.log" 2>&1 &
|
|
80
|
+
else
|
|
81
|
+
nohup bash -c "
|
|
82
|
+
trap 'source \"$PLUGIN_ROOT/scripts/lib/cartographer-lock.sh\"; cartographer_lock_release \"$LOCK_FILE\"' EXIT
|
|
83
|
+
source \"$PLUGIN_ROOT/scripts/lib/cartographer-config.sh\"
|
|
84
|
+
cartographer_config_load \"$REPO_ROOT\"
|
|
85
|
+
exec \"$PLUGIN_ROOT/scripts/run-audit.sh\"
|
|
86
|
+
" >>"$CARTOGRAPHER_DIR/audit.log" 2>&1 &
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
exit 0
|