@onlooker-community/ecosystem 0.14.0 → 0.15.1
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/plugin.json +1 -1
- package/.github/workflows/release.yml +8 -4
- package/.release-please-manifest.json +3 -2
- package/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/docs/architecture.md +28 -22
- package/package.json +1 -1
- 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/release-please-config.json +16 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
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",
|
|
@@ -33,19 +33,23 @@ jobs:
|
|
|
33
33
|
# Code marketplace, not npm — a release for a sibling alone must not
|
|
34
34
|
# trigger `npm publish`, which would attempt to re-publish ecosystem at
|
|
35
35
|
# its existing version and fail.
|
|
36
|
+
#
|
|
37
|
+
# We check paths_released (a JSON array) rather than the per-path output
|
|
38
|
+
# key `.--release_created` because keys starting with `.` are silently
|
|
39
|
+
# dropped when release-please-action writes them to $GITHUB_OUTPUT.
|
|
36
40
|
- uses: actions/checkout@v4
|
|
37
|
-
if: ${{ steps.release.outputs['
|
|
41
|
+
if: ${{ contains(fromJSON(steps.release.outputs.paths_released || '[]'), '.') }}
|
|
38
42
|
|
|
39
43
|
- uses: actions/setup-node@v4
|
|
40
|
-
if: ${{ steps.release.outputs['
|
|
44
|
+
if: ${{ contains(fromJSON(steps.release.outputs.paths_released || '[]'), '.') }}
|
|
41
45
|
with:
|
|
42
46
|
node-version: '22'
|
|
43
47
|
registry-url: 'https://registry.npmjs.org'
|
|
44
48
|
|
|
45
49
|
- run: npm ci
|
|
46
|
-
if: ${{ steps.release.outputs['
|
|
50
|
+
if: ${{ contains(fromJSON(steps.release.outputs.paths_released || '[]'), '.') }}
|
|
47
51
|
|
|
48
52
|
- run: npm publish
|
|
49
|
-
if: ${{ steps.release.outputs['
|
|
53
|
+
if: ${{ contains(fromJSON(steps.release.outputs.paths_released || '[]'), '.') }}
|
|
50
54
|
env:
|
|
51
55
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
|
10
|
+
## [0.15.1](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.15.0...ecosystem-v0.15.1) (2026-05-25)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **ci:** use paths_released to gate npm publish :rage: ([#37](https://github.com/onlooker-community/ecosystem/issues/37)) ([c62b17f](https://github.com/onlooker-community/ecosystem/commit/c62b17f7e1352cfe260a23c8f48be30f72edbbed))
|
|
16
|
+
|
|
17
|
+
## [0.15.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.14.0...ecosystem-v0.15.0) (2026-05-25)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **cartographer:** add proactive instruction-file audit plugin :mag: ([#35](https://github.com/onlooker-community/ecosystem/issues/35)) ([387d00a](https://github.com/onlooker-community/ecosystem/commit/387d00ad04da5aae91048254ad0526bb674ed498))
|
|
23
|
+
|
|
10
24
|
## [0.14.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.13.0...ecosystem-v0.14.0) (2026-05-25)
|
|
11
25
|
|
|
12
26
|
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The ecosystem is a **Claude Code plugin marketplace** built around a shared obse
|
|
|
16
16
|
| [`archivist`](./plugins/archivist) | Structured session memory across context truncation. Extracts decisions, dead ends, and open questions; reinjects the most relevant items at the start of the next session. | Yes — disabled by default |
|
|
17
17
|
| [`tribunal`](./plugins/tribunal) | Multi-agent quality gates. Wraps a task in an Actor → Jury → Meta-Judge → Gate loop; retries the Actor with critique until the gate passes or `max_iterations` is reached. | Yes — skill always available; Stop hook opt-in |
|
|
18
18
|
| [`echo`](./plugins/echo) | Prompt-change regression detection. When a watched agent file is modified, runs a quality pass and reports whether the change improved, degraded, or had no measurable effect. | Yes — disabled by default |
|
|
19
|
+
| [`cartographer`](./plugins/cartographer) | Proactive instruction-file auditor. Discovers all `CLAUDE.md`, `AGENTS.md`, and `.claude/rules/` files, maps their relationships, and surfaces contradictions, dead rules, stale references, and scope collisions before they cause agent misbehavior. | Yes — disabled by default |
|
|
19
20
|
|
|
20
21
|
For how these fit together, see [docs/architecture.md](docs/architecture.md).
|
|
21
22
|
|
package/docs/architecture.md
CHANGED
|
@@ -4,28 +4,27 @@ This document describes how the Onlooker ecosystem fits together: the shared sub
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
7
|
+
```mermaid
|
|
8
|
+
flowchart TB
|
|
9
|
+
subgraph session["Claude Code session"]
|
|
10
|
+
ecosystem["ecosystem<br/>(substrate)"]
|
|
11
|
+
archivist["archivist<br/>plugin"]
|
|
12
|
+
tribunal["tribunal<br/>plugin"]
|
|
13
|
+
echo["echo<br/>plugin"]
|
|
14
|
+
cartographer["cartographer<br/>plugin"]
|
|
15
|
+
|
|
16
|
+
emitter["onlooker-event.mjs<br/>(canonical emitter)"]
|
|
17
|
+
log["~/.onlooker/logs/<br/>onlooker-events.jsonl"]
|
|
18
|
+
|
|
19
|
+
ecosystem --> emitter
|
|
20
|
+
archivist --> emitter
|
|
21
|
+
tribunal --> emitter
|
|
22
|
+
echo --> emitter
|
|
23
|
+
cartographer --> emitter
|
|
24
|
+
|
|
25
|
+
emitter -->|"schema-validated<br/>event envelope"| log
|
|
26
|
+
log -.->|"append-only JSONL"| log
|
|
27
|
+
end
|
|
29
28
|
```
|
|
30
29
|
|
|
31
30
|
## The substrate layer: `ecosystem`
|
|
@@ -51,11 +50,18 @@ Plugins are independent packages under `plugins/<name>/`. Each has its own:
|
|
|
51
50
|
|
|
52
51
|
Plugins communicate by **emitting events**, not by calling each other directly. An Echo evaluation and a Tribunal jury run both write to the same JSONL log; a dashboard or downstream consumer can query across both.
|
|
53
52
|
|
|
53
|
+
### Cartographer
|
|
54
|
+
|
|
55
|
+
[Cartographer](../plugins/cartographer) is the only proactive plugin in the ecosystem. Rather than reacting to tool calls or session events, it runs a periodic background audit of your entire persistent instruction layer (`CLAUDE.md`, `AGENTS.md`, `.claude/rules/`). It surfaces four finding types — `contradiction`, `dead_rule`, `stale_ref`, and `scope_collision` — and emits each as a `cartographer.issue.found` event before the misbehavior they would cause ever occurs.
|
|
56
|
+
|
|
57
|
+
Findings are stored in `~/.onlooker/cartographer/<project-key>/findings/` and delivered at-least-once (deduplicated on `payload.finding_hash`). The audit runs as a detached background process; your session is never blocked.
|
|
58
|
+
|
|
54
59
|
### Plugin dependency model
|
|
55
60
|
|
|
56
61
|
All plugins depend on `ecosystem`. No plugin depends on another plugin at runtime. This means:
|
|
57
62
|
- Tribunal does not require Archivist to be installed.
|
|
58
63
|
- Echo does not require Tribunal to be installed (despite evaluating similar things — see [Echo ADR-002](../plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md)).
|
|
64
|
+
- Cartographer does not require any other plugin — it reads instruction files directly and emits events independently.
|
|
59
65
|
- You can install any subset of plugins and the others still work.
|
|
60
66
|
|
|
61
67
|
## The event bus
|
package/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cartographer",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Proactive periodic auditor of the persistent instruction layer (CLAUDE.md, AGENTS.md, .claude/rules/). Discovers all instruction files in the repo, extracts semantic maps, and surfaces contradictions, shadowing, gaps, and drift before they cause expensive agent misbehavior. 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": ["./skills/cartographer"],
|
|
13
|
+
"agents": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Cartographer plugin are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.2.0](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.1.0...cartographer-v0.2.0) (2026-05-25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **cartographer:** add proactive instruction-file audit plugin :mag: ([#35](https://github.com/onlooker-community/ecosystem/issues/35)) ([387d00a](https://github.com/onlooker-community/ecosystem/commit/387d00ad04da5aae91048254ad0526bb674ed498))
|
|
11
|
+
|
|
12
|
+
## [0.1.0](https://github.com/onlooker-community/ecosystem/releases/tag/cartographer-v0.1.0) (2026-05-25)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- SessionStart hook with interval gate and non-blocking background audit launch (`nohup setsid`)
|
|
17
|
+
- PostToolUse hook on Write/Edit/MultiEdit with exact `basename(realpath(...))` matching for CLAUDE.md files
|
|
18
|
+
- Five-phase audit pipeline: discover → extract → relate → synthesize → emit
|
|
19
|
+
- LLM-assisted analysis for contradictions, stale references, dead rules, and scope collisions
|
|
20
|
+
- `flock`-based cross-session audit lock with PID-file fallback for macOS
|
|
21
|
+
- Commutative `finding_hash` (SHA256) for stable finding identity across audit runs
|
|
22
|
+
- Atomic finding writes (`*.tmp` + `mv -f`) and `dedup/<hash>` sentinel store
|
|
23
|
+
- At-least-once `cartographer.issue.found` event delivery; documented dedup contract
|
|
24
|
+
- `/cartographer` skill with `--verbose`, `--status`, `--force`, `--scope`, and `--phase` flags
|
|
25
|
+
- Four ADRs documenting key design decisions
|
|
26
|
+
- Default `exclude_paths` covering `node_modules`, `.git`, `vendor`, `.venv`, and common build dirs
|
|
27
|
+
- `enabled: false` default — opt-in activation via `.claude/settings.json`
|
|
@@ -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
|