@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.
Files changed (129) hide show
  1. package/.claude-plugin/marketplace.json +39 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.github/copilot-instructions.md +46 -0
  4. package/.github/workflows/coverage.yml +78 -0
  5. package/.github/workflows/release.yml +24 -8
  6. package/.github/workflows/test.yml +3 -0
  7. package/.markdownlintignore +3 -0
  8. package/.release-please-manifest.json +5 -1
  9. package/CHANGELOG.md +44 -0
  10. package/README.md +58 -13
  11. package/config.json +6 -1
  12. package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
  13. package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
  14. package/docs/adr/003-ulid-over-uuid.md +40 -0
  15. package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
  16. package/docs/architecture.md +123 -0
  17. package/hooks/hooks.json +4 -0
  18. package/package.json +13 -7
  19. package/plugins/archivist/.claude-plugin/plugin.json +14 -0
  20. package/plugins/archivist/CHANGELOG.md +8 -0
  21. package/plugins/archivist/README.md +105 -0
  22. package/plugins/archivist/config.json +18 -0
  23. package/plugins/archivist/hooks/hooks.json +35 -0
  24. package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
  25. package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
  26. package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
  27. package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
  28. package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
  29. package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
  30. package/plugins/cartographer/.claude-plugin/plugin.json +14 -0
  31. package/plugins/cartographer/CHANGELOG.md +27 -0
  32. package/plugins/cartographer/README.md +113 -0
  33. package/plugins/cartographer/config.json +21 -0
  34. package/plugins/cartographer/docs/adr/001-background-audit-launch.md +28 -0
  35. package/plugins/cartographer/docs/adr/002-flock-pid-file-fallback.md +30 -0
  36. package/plugins/cartographer/docs/adr/003-at-least-once-event-delivery.md +32 -0
  37. package/plugins/cartographer/docs/adr/004-exclude-paths-replace-semantics.md +27 -0
  38. package/plugins/cartographer/hooks/hooks.json +44 -0
  39. package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +87 -0
  40. package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +89 -0
  41. package/plugins/cartographer/scripts/lib/cartographer-analyze.sh +286 -0
  42. package/plugins/cartographer/scripts/lib/cartographer-collect.sh +59 -0
  43. package/plugins/cartographer/scripts/lib/cartographer-config.sh +105 -0
  44. package/plugins/cartographer/scripts/lib/cartographer-events.sh +82 -0
  45. package/plugins/cartographer/scripts/lib/cartographer-lock.sh +38 -0
  46. package/plugins/cartographer/scripts/lib/cartographer-project-key.sh +55 -0
  47. package/plugins/cartographer/scripts/lib/cartographer-ulid.sh +47 -0
  48. package/plugins/cartographer/scripts/run-audit.sh +309 -0
  49. package/plugins/cartographer/skills/cartographer/SKILL.md +154 -0
  50. package/plugins/echo/.claude-plugin/plugin.json +14 -0
  51. package/plugins/echo/CHANGELOG.md +24 -0
  52. package/plugins/echo/README.md +110 -0
  53. package/plugins/echo/config.json +15 -0
  54. package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
  55. package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
  56. package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
  57. package/plugins/echo/hooks/hooks.json +15 -0
  58. package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
  59. package/plugins/echo/scripts/lib/echo-config.sh +108 -0
  60. package/plugins/echo/scripts/lib/echo-events.sh +74 -0
  61. package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
  62. package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
  63. package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
  64. package/plugins/tribunal/CHANGELOG.md +10 -0
  65. package/plugins/tribunal/README.md +134 -0
  66. package/plugins/tribunal/agents/tribunal-actor.md +35 -0
  67. package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
  68. package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
  69. package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
  70. package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
  71. package/plugins/tribunal/config.json +50 -0
  72. package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
  73. package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
  74. package/plugins/tribunal/hooks/hooks.json +15 -0
  75. package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
  76. package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
  77. package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
  78. package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
  79. package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
  80. package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
  81. package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
  82. package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
  83. package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
  84. package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
  85. package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
  86. package/release-please-config.json +59 -5
  87. package/scripts/coverage/bash-coverage.mjs +169 -0
  88. package/scripts/coverage/format-comment.mjs +120 -0
  89. package/scripts/coverage/run-coverage.mjs +151 -0
  90. package/scripts/hooks/agent-spawn-tracker.sh +4 -4
  91. package/scripts/hooks/prompt-rule-injector.sh +122 -0
  92. package/scripts/lib/portable-lock.sh +48 -0
  93. package/scripts/lib/prompt-rules.sh +207 -0
  94. package/scripts/lib/tool-history.sh +7 -8
  95. package/scripts/lib/validate-path.sh +4 -0
  96. package/scripts/lint/check-manifests.mjs +314 -0
  97. package/scripts/lint/check-references.mjs +311 -0
  98. package/skills/list-prompt-rules/SKILL.md +15 -0
  99. package/test/bats/archivist-config-files.bats +60 -0
  100. package/test/bats/archivist-config.bats +54 -0
  101. package/test/bats/archivist-inject.bats +73 -0
  102. package/test/bats/archivist-project-key.bats +75 -0
  103. package/test/bats/archivist-storage.bats +119 -0
  104. package/test/bats/archivist-ulid.bats +36 -0
  105. package/test/bats/cartographer-config.bats +107 -0
  106. package/test/bats/cartographer-lock.bats +77 -0
  107. package/test/bats/cartographer-ulid.bats +56 -0
  108. package/test/bats/config.bats +10 -10
  109. package/test/bats/echo-config.bats +90 -0
  110. package/test/bats/echo-events.bats +121 -0
  111. package/test/bats/echo-project-key.bats +115 -0
  112. package/test/bats/echo-stop-hook.bats +101 -0
  113. package/test/bats/echo-ulid.bats +38 -0
  114. package/test/bats/portable-lock.bats +62 -0
  115. package/test/bats/prompt-rules.bats +269 -0
  116. package/test/bats/tribunal-aggregate.bats +77 -0
  117. package/test/bats/tribunal-config.bats +86 -0
  118. package/test/bats/tribunal-events.bats +209 -0
  119. package/test/bats/tribunal-gate.bats +95 -0
  120. package/test/bats/tribunal-jury.bats +80 -0
  121. package/test/bats/tribunal-rubric.bats +119 -0
  122. package/test/bats/tribunal-stop-hook.bats +73 -0
  123. package/test/bats/tribunal-verdict.bats +71 -0
  124. package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
  125. package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
  126. package/test/helpers/setup.bash +9 -0
  127. package/test/node/check-manifests.test.mjs +173 -0
  128. package/test/node/check-references.test.mjs +279 -0
  129. 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