@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
|
@@ -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
|
+
}
|