@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.
Files changed (31) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.github/workflows/release.yml +8 -4
  3. package/.release-please-manifest.json +3 -2
  4. package/CHANGELOG.md +14 -0
  5. package/README.md +1 -0
  6. package/docs/architecture.md +28 -22
  7. package/package.json +1 -1
  8. package/plugins/cartographer/.claude-plugin/plugin.json +14 -0
  9. package/plugins/cartographer/CHANGELOG.md +27 -0
  10. package/plugins/cartographer/README.md +113 -0
  11. package/plugins/cartographer/config.json +21 -0
  12. package/plugins/cartographer/docs/adr/001-background-audit-launch.md +28 -0
  13. package/plugins/cartographer/docs/adr/002-flock-pid-file-fallback.md +30 -0
  14. package/plugins/cartographer/docs/adr/003-at-least-once-event-delivery.md +32 -0
  15. package/plugins/cartographer/docs/adr/004-exclude-paths-replace-semantics.md +27 -0
  16. package/plugins/cartographer/hooks/hooks.json +44 -0
  17. package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +87 -0
  18. package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +89 -0
  19. package/plugins/cartographer/scripts/lib/cartographer-analyze.sh +286 -0
  20. package/plugins/cartographer/scripts/lib/cartographer-collect.sh +59 -0
  21. package/plugins/cartographer/scripts/lib/cartographer-config.sh +105 -0
  22. package/plugins/cartographer/scripts/lib/cartographer-events.sh +82 -0
  23. package/plugins/cartographer/scripts/lib/cartographer-lock.sh +38 -0
  24. package/plugins/cartographer/scripts/lib/cartographer-project-key.sh +55 -0
  25. package/plugins/cartographer/scripts/lib/cartographer-ulid.sh +47 -0
  26. package/plugins/cartographer/scripts/run-audit.sh +309 -0
  27. package/plugins/cartographer/skills/cartographer/SKILL.md +154 -0
  28. package/release-please-config.json +16 -0
  29. package/test/bats/cartographer-config.bats +107 -0
  30. package/test/bats/cartographer-lock.bats +77 -0
  31. package/test/bats/cartographer-ulid.bats +56 -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
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env bash
2
+ # cartographer-analyze.sh — LLM-assisted analysis of instruction files.
3
+ #
4
+ # Orchestrates four analysis phases:
5
+ # 1. contradiction — rules that cannot both be satisfied simultaneously
6
+ # 2. stale_ref — references to paths/tools that no longer exist
7
+ # 3. dead_rule — rules subsumed elsewhere or referencing removed workflows
8
+ # 4. scope_collision — project rules duplicating or contradicting global rules
9
+ #
10
+ # Each phase calls `claude -p` and produces a JSON findings array.
11
+ # Findings are normalized and returned for deduplication + storage.
12
+ #
13
+ # Usage:
14
+ # cartographer_analyze_contradiction <files_json> <model> <max_tokens> <phase_timeout>
15
+ # cartographer_analyze_stale_ref <files_json> <repo_root> <model> <max_tokens> <phase_timeout>
16
+ # cartographer_analyze_scope_collision <global_files_json> <project_files_json> <model> <max_tokens> <phase_timeout>
17
+ #
18
+ # Note: contradiction detection also flags dead_rule findings in a single LLM pass.
19
+ #
20
+ # Each function prints a JSON array of finding objects on stdout:
21
+ # [{type, severity, file_a, excerpt_a, file_b, excerpt_b, description, suggested_fix}]
22
+
23
+ _CARTOGRAPHER_TIMEOUT_CMD=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || printf 'timeout')
24
+
25
+ _cartographer_read_files_for_prompt() {
26
+ local files_json="$1"
27
+ local output=""
28
+ while IFS= read -r fpath; do
29
+ [[ -z "$fpath" || ! -f "$fpath" ]] && continue
30
+ output+=$'\n<FILE: '"$fpath"$'>\n'
31
+ output+=$(cat "$fpath")
32
+ output+=$'\n</FILE>\n'
33
+ done < <(printf '%s' "$files_json" | jq -r '.[]' 2>/dev/null)
34
+ printf '%s' "$output"
35
+ }
36
+
37
+ cartographer_analyze_contradiction() {
38
+ local files_json="$1"
39
+ local model="${2:-claude-haiku-4-5-20251001}"
40
+ local max_tokens="${3:-2048}"
41
+ local timeout_s="${4:-60}"
42
+
43
+ local corpus
44
+ corpus=$(_cartographer_read_files_for_prompt "$files_json")
45
+ [[ -z "$corpus" ]] && printf '[]' && return 0
46
+
47
+ local prompt
48
+ prompt=$(cat <<'PROMPT'
49
+ You are an expert technical editor reviewing Claude Code instruction files for internal consistency.
50
+
51
+ Your task: identify any CONTRADICTIONS — pairs of rules that cannot both be satisfied at the same time. A contradiction exists only when following rule A makes it impossible or directly inconsistent to follow rule B. Do not flag rules that are merely different or address different contexts.
52
+
53
+ Also identify any DEAD RULES — rules that are fully redundant because a more specific rule elsewhere already covers exactly the same ground.
54
+
55
+ Output ONLY a JSON array. Each element:
56
+ {
57
+ "type": "contradiction" | "dead_rule",
58
+ "severity": "error" | "warning",
59
+ "file_a": "<absolute path>",
60
+ "excerpt_a": "<quoted text ≤150 chars>",
61
+ "file_b": "<absolute path>",
62
+ "excerpt_b": "<quoted text ≤150 chars>",
63
+ "description": "<one sentence explaining the conflict or redundancy>",
64
+ "suggested_fix": "<one sentence concrete action>"
65
+ }
66
+
67
+ Severity: "error" if following both rules would violate safety or produce incorrect output; "warning" otherwise.
68
+ If no issues found, output: []
69
+ PROMPT
70
+ )
71
+
72
+ local full_prompt="${prompt}
73
+
74
+ ${corpus}"
75
+
76
+ local response
77
+ response=$(printf '%s' "$full_prompt" \
78
+ | $_CARTOGRAPHER_TIMEOUT_CMD "$timeout_s" claude -p \
79
+ --model "$model" \
80
+ --max-tokens "$max_tokens" \
81
+ 2>/dev/null) || { printf '[]'; return 1; }
82
+
83
+ printf '%s' "$response" | python3 -c "
84
+ import sys, json
85
+ raw = sys.stdin.read()
86
+ start = raw.find('[')
87
+ end = raw.rfind(']') + 1
88
+ if start < 0 or end <= start:
89
+ print('[]')
90
+ else:
91
+ try:
92
+ arr = json.loads(raw[start:end])
93
+ print(json.dumps(arr))
94
+ except Exception:
95
+ print('[]')
96
+ " 2>/dev/null || printf '[]'
97
+ }
98
+
99
+ cartographer_analyze_stale_ref() {
100
+ local files_json="$1"
101
+ local repo_root="$2"
102
+ local model="${3:-claude-haiku-4-5-20251001}"
103
+ local max_tokens="${4:-2048}"
104
+ local timeout_s="${5:-60}"
105
+
106
+ # bash pre-pass: extract path-like tokens and test on filesystem
107
+ local candidates=""
108
+ while IFS= read -r fpath; do
109
+ [[ -z "$fpath" || ! -f "$fpath" ]] && continue
110
+ local line_no=0
111
+ while IFS= read -r line; do
112
+ (( line_no++ ))
113
+ # extract tokens that look like relative/absolute paths
114
+ local tokens
115
+ tokens=$(printf '%s' "$line" | grep -oE '[./][a-zA-Z0-9_/.-]{3,}' 2>/dev/null || true)
116
+ while IFS= read -r tok; do
117
+ [[ -z "$tok" ]] && continue
118
+ # resolve relative to repo root
119
+ local resolved="$repo_root/$tok"
120
+ if [[ ! -e "$resolved" && ! -e "$tok" ]]; then
121
+ candidates+="FILE=$fpath LINE=$line_no TOKEN=$tok CONTEXT=$(printf '%s' "$line" | cut -c1-120)"$'\n'
122
+ fi
123
+ done < <(printf '%s\n' "$tokens")
124
+ done <"$fpath"
125
+ done < <(printf '%s' "$files_json" | jq -r '.[]' 2>/dev/null)
126
+
127
+ [[ -z "$candidates" ]] && printf '[]' && return 0
128
+
129
+ local prompt
130
+ prompt=$(cat <<'PROMPT'
131
+ You are reviewing references extracted from Claude Code instruction files. Each reference could not be resolved on the filesystem.
132
+
133
+ Classify each as:
134
+ - "stale": a genuine reference to something that should exist but does not
135
+ - "example": an illustrative or hypothetical path, not a real reference
136
+ - "ambiguous": cannot tell from context
137
+
138
+ Output ONLY a JSON array (only include items classified as "stale"):
139
+ {
140
+ "type": "stale_ref",
141
+ "severity": "warning",
142
+ "file_a": "<path>",
143
+ "excerpt_a": "<the unresolvable reference token>",
144
+ "file_b": null,
145
+ "excerpt_b": null,
146
+ "description": "<one sentence>",
147
+ "suggested_fix": "<one sentence, or null>"
148
+ }
149
+
150
+ If none are stale, output: []
151
+ PROMPT
152
+ )
153
+ local full_prompt="${prompt}
154
+
155
+ <CANDIDATES>
156
+ ${candidates}
157
+ </CANDIDATES>"
158
+
159
+ local response
160
+ response=$(printf '%s' "$full_prompt" \
161
+ | $_CARTOGRAPHER_TIMEOUT_CMD "$timeout_s" claude -p \
162
+ --model "$model" \
163
+ --max-tokens "$max_tokens" \
164
+ 2>/dev/null) || { printf '[]'; return 1; }
165
+
166
+ printf '%s' "$response" | python3 -c "
167
+ import sys, json
168
+ raw = sys.stdin.read()
169
+ start = raw.find('[')
170
+ end = raw.rfind(']') + 1
171
+ if start < 0 or end <= start:
172
+ print('[]')
173
+ else:
174
+ try:
175
+ arr = json.loads(raw[start:end])
176
+ print(json.dumps(arr))
177
+ except Exception:
178
+ print('[]')
179
+ " 2>/dev/null || printf '[]'
180
+ }
181
+
182
+ cartographer_analyze_scope_collision() {
183
+ local global_json="$1"
184
+ local project_json="$2"
185
+ local model="${3:-claude-haiku-4-5-20251001}"
186
+ local max_tokens="${4:-2048}"
187
+ local timeout_s="${5:-60}"
188
+
189
+ local global_files project_files
190
+ global_files=$(_cartographer_read_files_for_prompt "$global_json")
191
+ project_files=$(_cartographer_read_files_for_prompt "$project_json")
192
+
193
+ [[ -z "$global_files" || -z "$project_files" ]] && printf '[]' && return 0
194
+
195
+ local prompt
196
+ prompt=$(cat <<'PROMPT'
197
+ You are auditing Claude Code instruction files for scope collisions between global and project-level files.
198
+
199
+ The GLOBAL file applies to all projects. The PROJECT files apply only to this project. Project rules override global rules when they conflict.
200
+
201
+ Identify SCOPE COLLISIONS:
202
+ 1. Exact or near-exact duplications where the project rule adds nothing new
203
+ 2. Contradictions where it is unclear from context that the project is intentionally overriding the global
204
+
205
+ Do NOT flag intentional, clearly-worded overrides like "For this project, use X instead of the global default Y".
206
+
207
+ Output ONLY a JSON array:
208
+ {
209
+ "type": "scope_collision",
210
+ "severity": "warning",
211
+ "file_a": "<global file path>",
212
+ "excerpt_a": "<global rule ≤150 chars>",
213
+ "file_b": "<project file path>",
214
+ "excerpt_b": "<project rule ≤150 chars>",
215
+ "description": "<one sentence>",
216
+ "suggested_fix": "<one sentence>"
217
+ }
218
+
219
+ If none found, output: []
220
+ PROMPT
221
+ )
222
+
223
+ local full_prompt="${prompt}
224
+
225
+ <GLOBAL FILES>
226
+ ${global_files}
227
+ </GLOBAL FILES>
228
+
229
+ <PROJECT FILES>
230
+ ${project_files}
231
+ </PROJECT FILES>"
232
+
233
+ local response
234
+ response=$(printf '%s' "$full_prompt" \
235
+ | $_CARTOGRAPHER_TIMEOUT_CMD "$timeout_s" claude -p \
236
+ --model "$model" \
237
+ --max-tokens "$max_tokens" \
238
+ 2>/dev/null) || { printf '[]'; return 1; }
239
+
240
+ printf '%s' "$response" | python3 -c "
241
+ import sys, json
242
+ raw = sys.stdin.read()
243
+ start = raw.find('[')
244
+ end = raw.rfind(']') + 1
245
+ if start < 0 or end <= start:
246
+ print('[]')
247
+ else:
248
+ try:
249
+ arr = json.loads(raw[start:end])
250
+ print(json.dumps(arr))
251
+ except Exception:
252
+ print('[]')
253
+ " 2>/dev/null || printf '[]'
254
+ }
255
+
256
+ # Compute the canonical finding hash (commutative across file_a/file_b).
257
+ cartographer_finding_hash() {
258
+ local type="$1"
259
+ local file_a="$2"
260
+ local excerpt_a="$3"
261
+ local file_b="${4:-}"
262
+ local excerpt_b="${5:-}"
263
+
264
+ # Sort files so A→B and B→A produce the same hash
265
+ local sorted_files
266
+ if [[ "$file_a" < "$file_b" ]]; then
267
+ sorted_files="${file_a}:::${file_b}"
268
+ else
269
+ sorted_files="${file_b}:::${file_a}"
270
+ fi
271
+
272
+ # Normalize excerpts: strip leading/trailing whitespace, collapse internal runs
273
+ local norm_a norm_b
274
+ norm_a=$(printf '%s' "$excerpt_a" | tr -s ' \t\n' ' ' | sed 's/^ //;s/ $//')
275
+ norm_b=$(printf '%s' "$excerpt_b" | tr -s ' \t\n' ' ' | sed 's/^ //;s/ $//')
276
+
277
+ local input="${type}:${sorted_files}:${norm_a}:${norm_b}"
278
+ if command -v sha256sum &>/dev/null; then
279
+ printf '%s' "$input" | sha256sum | cut -c1-16
280
+ elif command -v shasum &>/dev/null; then
281
+ printf '%s' "$input" | shasum -a 256 | cut -c1-16
282
+ else
283
+ printf '%s' "$input" | python3 -c \
284
+ 'import sys,hashlib; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:16])'
285
+ fi
286
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ # cartographer-collect.sh — discover all auditable instruction files under a repo root.
3
+ #
4
+ # Finds:
5
+ # - CLAUDE.md files (all depths up to max_depth)
6
+ # - AGENTS.md files (all depths up to max_depth)
7
+ # - .claude/rules/*.md files (global and repo-level)
8
+ #
9
+ # Applies exclude_paths (substring filter) to the collected list.
10
+ #
11
+ # Usage:
12
+ # cartographer_collect_files <repo_root> <exclude_paths_json> [max_depth]
13
+ # # prints one absolute path per line
14
+
15
+ cartographer_collect_files() {
16
+ local repo_root="${1:?repo_root required}"
17
+ local exclude_json="${2:-[]}"
18
+ local max_depth="${3:-5}"
19
+
20
+ # Build find -not -path exclusions
21
+ local find_excludes=()
22
+ while IFS= read -r excl; do
23
+ [[ -z "$excl" ]] && continue
24
+ find_excludes+=(-not -path "*/${excl}/*")
25
+ done < <(printf '%s' "$exclude_json" | jq -r '.[]' 2>/dev/null)
26
+
27
+ # Discover CLAUDE.md and AGENTS.md
28
+ find "$repo_root" -maxdepth "$max_depth" \
29
+ \( -name "CLAUDE.md" -o -name "AGENTS.md" \) \
30
+ "${find_excludes[@]}" \
31
+ -type f 2>/dev/null
32
+
33
+ # Discover .claude/rules/*.md at repo level
34
+ if [[ -d "$repo_root/.claude/rules" ]]; then
35
+ find "$repo_root/.claude/rules" -maxdepth 2 -type f -name "*.md" 2>/dev/null
36
+ fi
37
+ }
38
+
39
+ cartographer_collect_global_files() {
40
+ # Global ~/.claude/CLAUDE.md and ~/.claude/rules/*.md
41
+ if [[ -f "$HOME/.claude/CLAUDE.md" ]]; then
42
+ printf '%s\n' "$HOME/.claude/CLAUDE.md"
43
+ fi
44
+ if [[ -d "$HOME/.claude/rules" ]]; then
45
+ find "$HOME/.claude/rules" -maxdepth 2 -type f -name "*.md" 2>/dev/null
46
+ fi
47
+ }
48
+
49
+ cartographer_file_content_hash() {
50
+ local path="$1"
51
+ [[ ! -f "$path" ]] && return 1
52
+ if command -v sha256sum &>/dev/null; then
53
+ sha256sum "$path" | cut -c1-16
54
+ elif command -v shasum &>/dev/null; then
55
+ shasum -a 256 "$path" | cut -c1-16
56
+ else
57
+ python3 -c "import sys,hashlib; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest()[:16])" "$path"
58
+ fi
59
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env bash
2
+ # cartographer-config.sh — load and query Cartographer configuration.
3
+ #
4
+ # Merges three layers in precedence order (later wins):
5
+ # 1. plugins/cartographer/config.json (plugin defaults)
6
+ # 2. ~/.claude/settings.json (.cartographer subtree)
7
+ # 3. <repo>/.claude/settings.json (.cartographer subtree)
8
+ #
9
+ # Usage:
10
+ # cartographer_config_load <repo_root>
11
+ # cartographer_config_get ".cartographer.enabled"
12
+ # cartographer_config_get_json ".cartographer.exclude_paths"
13
+
14
+ _CARTOGRAPHER_CONFIG=""
15
+ _CARTOGRAPHER_PLUGIN_CONFIG=""
16
+
17
+ cartographer_config_load() {
18
+ local repo_root="${1:-}"
19
+ local plugin_dir
20
+ plugin_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
21
+ local plugin_config="$plugin_dir/config.json"
22
+
23
+ _CARTOGRAPHER_PLUGIN_CONFIG="{}"
24
+ if [[ -f "$plugin_config" ]]; then
25
+ _CARTOGRAPHER_PLUGIN_CONFIG=$(cat "$plugin_config")
26
+ fi
27
+
28
+ local home_settings="{}"
29
+ if [[ -f "$HOME/.claude/settings.json" ]]; then
30
+ home_settings=$(cat "$HOME/.claude/settings.json")
31
+ fi
32
+
33
+ local repo_settings="{}"
34
+ if [[ -n "$repo_root" && -f "$repo_root/.claude/settings.json" ]]; then
35
+ repo_settings=$(cat "$repo_root/.claude/settings.json")
36
+ fi
37
+
38
+ _CARTOGRAPHER_CONFIG=$(jq -n \
39
+ --argjson plugin "$_CARTOGRAPHER_PLUGIN_CONFIG" \
40
+ --argjson home "$home_settings" \
41
+ --argjson repo "$repo_settings" \
42
+ '$plugin * {"cartographer": (($plugin.cartographer // {}) * ($home.cartographer // {}) * ($repo.cartographer // {}))}')
43
+ }
44
+
45
+ cartographer_config_get() {
46
+ local path="${1:-}"
47
+ printf '%s' "$_CARTOGRAPHER_CONFIG" | jq -r "$path // empty" 2>/dev/null
48
+ }
49
+
50
+ cartographer_config_get_json() {
51
+ local path="${1:-}"
52
+ printf '%s' "$_CARTOGRAPHER_CONFIG" | jq -c "$path // empty" 2>/dev/null
53
+ }
54
+
55
+ cartographer_config_enabled() {
56
+ local v
57
+ v=$(cartographer_config_get '.cartographer.enabled')
58
+ [[ "$v" == "true" ]]
59
+ }
60
+
61
+ cartographer_config_model_extraction() {
62
+ local v
63
+ v=$(cartographer_config_get '.cartographer.extraction.model')
64
+ printf '%s' "${v:-claude-haiku-4-5-20251001}"
65
+ }
66
+
67
+ cartographer_config_model_synthesis() {
68
+ local v
69
+ v=$(cartographer_config_get '.cartographer.synthesis.model')
70
+ printf '%s' "${v:-claude-haiku-4-5-20251001}"
71
+ }
72
+
73
+ cartographer_config_phase_timeout() {
74
+ local v
75
+ v=$(cartographer_config_get '.cartographer.phase_timeout_seconds')
76
+ printf '%s' "${v:-60}"
77
+ }
78
+
79
+ cartographer_config_total_timeout() {
80
+ local v
81
+ v=$(cartographer_config_get '.cartographer.total_timeout_seconds')
82
+ printf '%s' "${v:-600}"
83
+ }
84
+
85
+ cartographer_config_audit_interval_hours() {
86
+ local v
87
+ v=$(cartographer_config_get '.cartographer.audit_interval_hours')
88
+ printf '%s' "${v:-24}"
89
+ }
90
+
91
+ cartographer_config_exclude_paths() {
92
+ cartographer_config_get_json '.cartographer.exclude_paths // ["node_modules",".git","vendor",".venv","dist",".next",".nuxt","build","__pycache__"]'
93
+ }
94
+
95
+ cartographer_config_max_output_tokens_extraction() {
96
+ local v
97
+ v=$(cartographer_config_get '.cartographer.extraction.max_output_tokens')
98
+ printf '%s' "${v:-2048}"
99
+ }
100
+
101
+ cartographer_config_max_output_tokens_synthesis() {
102
+ local v
103
+ v=$(cartographer_config_get '.cartographer.synthesis.max_output_tokens')
104
+ printf '%s' "${v:-2048}"
105
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # cartographer-events.sh — emit cartographer.* events to the canonical event log.
3
+ #
4
+ # Thin wrapper around onlooker-event.mjs. Validation failures are logged to
5
+ # stderr and do not abort the caller — Cartographer is advisory.
6
+ #
7
+ # Usage:
8
+ # cartographer_emit_event "cartographer.audit.complete" '{"audit_id":"...","new_finding_count":2}'
9
+
10
+ _CARTOGRAPHER_PLUGIN_NAME="cartographer"
11
+
12
+ _cartographer_event_js_path() {
13
+ if [[ -n "${_ONLOOKER_EVENT_JS:-}" && -f "$_ONLOOKER_EVENT_JS" ]]; then
14
+ printf '%s' "$_ONLOOKER_EVENT_JS"
15
+ return 0
16
+ fi
17
+ local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
18
+ local candidates=(
19
+ "${plugin_root}/scripts/lib/onlooker-event.mjs"
20
+ "${plugin_root}/../../scripts/lib/onlooker-event.mjs"
21
+ )
22
+ local c
23
+ for c in "${candidates[@]}"; do
24
+ [[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
25
+ done
26
+ return 1
27
+ }
28
+
29
+ _cartographer_session_id() {
30
+ if [[ -n "${_HOOK_SESSION_ID:-}" ]]; then
31
+ printf '%s' "$_HOOK_SESSION_ID"
32
+ return 0
33
+ fi
34
+ if [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then
35
+ printf '%s' "$CLAUDE_SESSION_ID"
36
+ return 0
37
+ fi
38
+ printf 'unknown'
39
+ }
40
+
41
+ cartographer_emit_event() {
42
+ local event_type="${1:-}"
43
+ local payload="${2:-}"
44
+ [[ -z "$event_type" || -z "$payload" ]] && return 1
45
+
46
+ local event_js
47
+ event_js=$(_cartographer_event_js_path) || {
48
+ printf 'cartographer-events: cannot locate onlooker-event.mjs\n' >&2
49
+ return 1
50
+ }
51
+
52
+ local session_id
53
+ session_id=$(_cartographer_session_id)
54
+
55
+ local params
56
+ params=$(jq -n \
57
+ --arg plugin "$_CARTOGRAPHER_PLUGIN_NAME" \
58
+ --arg sid "$session_id" \
59
+ --arg type "$event_type" \
60
+ --argjson payload "$payload" \
61
+ '{"plugin":$plugin,"session_id":$sid,"event_type":$type,"payload":$payload}')
62
+
63
+ local stderr_file
64
+ stderr_file=$(mktemp -t cartographer-event-err.XXXXXX 2>/dev/null) \
65
+ || stderr_file="/tmp/cartographer-event-err.$$"
66
+
67
+ local event
68
+ event=$(printf '%s' "$params" \
69
+ | ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}" \
70
+ ONLOOKER_PLUGIN_NAME="$_CARTOGRAPHER_PLUGIN_NAME" \
71
+ node "$event_js" emit 2>"$stderr_file") || {
72
+ printf 'cartographer-events: schema validation failed for %s\n' "$event_type" >&2
73
+ [[ -s "$stderr_file" ]] && cat "$stderr_file" >&2
74
+ rm -f "$stderr_file"
75
+ return 1
76
+ }
77
+ rm -f "$stderr_file"
78
+
79
+ local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
80
+ mkdir -p "$(dirname "$log_path")" 2>/dev/null || return 1
81
+ printf '%s\n' "$event" >>"$log_path"
82
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # cartographer-lock.sh — thin wrappers around the shared portable-lock.sh.
3
+ #
4
+ # portable-lock.sh uses atomic mkdir() which works on Linux, macOS, and any
5
+ # POSIX local filesystem without requiring flock or any external utility.
6
+ #
7
+ # Usage:
8
+ # source cartographer-lock.sh
9
+ # cartographer_lock_acquire <lock_file> # returns 0=acquired, 1=timeout
10
+ # cartographer_lock_release <lock_file>
11
+
12
+ _CARTOGRAPHER_LOCK_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../scripts/lib" && pwd)/portable-lock.sh"
13
+
14
+ if [[ ! -f "$_CARTOGRAPHER_LOCK_LIB" ]]; then
15
+ printf '[cartographer-lock] ERROR: portable-lock.sh not found at %s\n' \
16
+ "$_CARTOGRAPHER_LOCK_LIB" >&2
17
+ exit 1
18
+ fi
19
+
20
+ # shellcheck source=../../../../scripts/lib/portable-lock.sh
21
+ source "$_CARTOGRAPHER_LOCK_LIB"
22
+
23
+ cartographer_lock_acquire() {
24
+ local lock_file="${1:?lock_file required}"
25
+ mkdir -p "$(dirname "$lock_file")" 2>/dev/null || true
26
+ # Non-blocking: pass timeout=0 so we return immediately if held.
27
+ lock_acquire "$lock_file" 0
28
+ }
29
+
30
+ cartographer_lock_release() {
31
+ local lock_file="${1:?lock_file required}"
32
+ lock_release "$lock_file"
33
+ }
34
+
35
+ cartographer_lock_is_held() {
36
+ local lock_file="${1:?lock_file required}"
37
+ [[ -d "${lock_file}.d" ]]
38
+ }