@onlooker-community/ecosystem 0.43.5 → 0.43.7
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/.release-please-manifest.json +3 -3
- package/CHANGELOG.md +15 -0
- package/docs/architecture.md +2 -2
- package/package.json +1 -1
- package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
- package/plugins/cartographer/CHANGELOG.md +7 -0
- package/plugins/cartographer/scripts/run-audit.sh +23 -1
- package/plugins/inspector/.claude-plugin/plugin.json +1 -1
- package/plugins/inspector/CHANGELOG.md +7 -0
- package/plugins/inspector/scripts/hooks/inspector-post-write.sh +4 -8
- package/plugins/inspector/scripts/lib/inspector-project-key.sh +59 -2
- package/scripts/lib/validate-path.sh +29 -38
- package/test/bats/cartographer-run-audit.bats +50 -4
- package/test/bats/inspector-post-write-hook.bats +92 -0
- package/test/bats/validate-path.bats +46 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecosystem",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.7",
|
|
4
4
|
"description": "Observability substrate for Claude Code. Provides the shared $ONLOOKER_DIR storage root (default $HOME/.onlooker), 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",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
".": "0.43.
|
|
2
|
+
".": "0.43.7",
|
|
3
3
|
"plugins/archivist": "0.3.2",
|
|
4
4
|
"plugins/tribunal": "1.2.8",
|
|
5
5
|
"plugins/echo": "0.3.2",
|
|
6
|
-
"plugins/cartographer": "0.6.
|
|
6
|
+
"plugins/cartographer": "0.6.2",
|
|
7
7
|
"plugins/governor": "0.3.3",
|
|
8
8
|
"plugins/compass": "0.4.2",
|
|
9
9
|
"plugins/scribe": "0.4.3",
|
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
"plugins/assayer": "1.1.4",
|
|
16
16
|
"plugins/bursar": "0.3.1",
|
|
17
17
|
"plugins/lineage": "0.2.3",
|
|
18
|
-
"plugins/inspector": "0.3.
|
|
18
|
+
"plugins/inspector": "0.3.4"
|
|
19
19
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.43.7](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.6...ecosystem-v0.43.7) (2026-08-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **cartographer:** drop a typeless finding instead of hashing it as "unknown" :broom: ([#203](https://github.com/onlooker-community/ecosystem/issues/203)) ([a949d5d](https://github.com/onlooker-community/ecosystem/commit/a949d5d43f3ae3515ac6278b35d1be5cc468cb0e))
|
|
9
|
+
|
|
10
|
+
## [0.43.6](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.5...ecosystem-v0.43.6) (2026-08-22)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **events:** drop safe_emit's hand-built envelope fallback :broom: ([#202](https://github.com/onlooker-community/ecosystem/issues/202)) ([bad684a](https://github.com/onlooker-community/ecosystem/commit/bad684a4f7c535d72f91ed2b1d4928a4dd9a10e8))
|
|
16
|
+
* **inspector:** canonicalize the file and repo root the same way :mag: ([0f850e9](https://github.com/onlooker-community/ecosystem/commit/0f850e92d7e620e4d7e06f512ef81efe4255f316))
|
|
17
|
+
|
|
3
18
|
## [0.43.5](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.43.4...ecosystem-v0.43.5) (2026-08-22)
|
|
4
19
|
|
|
5
20
|
|
package/docs/architecture.md
CHANGED
|
@@ -93,9 +93,9 @@ Every observable event flows through `onlooker-event.mjs` before being written t
|
|
|
93
93
|
|
|
94
94
|
The schema is versioned independently and published to npm. Plugin shell scripts invoke `onlooker-event.mjs` at runtime so schema validation always reflects the installed version.
|
|
95
95
|
|
|
96
|
-
> **Note:** Every emission path
|
|
96
|
+
> **Note:** Every emission path in this repo routes through `onlooker-event.mjs`. Nothing hand-builds an envelope and appends it to the log. Two paths used to: `prompt_rules_emit` (ecosystem-aaz) and the `safe_emit` fallback in `scripts/lib/validate-path.sh` (ecosystem-0tm). Both wrote lines missing all five required `id`/`schema_version`/`runtime`/`machine_id`/`sequence` fields, plus fields the envelope forbids — `event.v1.json` is `additionalProperties: false`.
|
|
97
97
|
>
|
|
98
|
-
>
|
|
98
|
+
> Route a new event type the same way rather than writing to the log directly. The shape to copy is a `{plugin, session_id, event_type, payload}` params object piped to `onlooker-event.mjs emit`, whose output you append; the emitter owns envelope assembly so there is exactly one place for it to drift. When the emitter is unavailable, emit nothing and return non-zero — an unparseable line on the bus is worse than a missing one.
|
|
99
99
|
|
|
100
100
|
### Emission gates
|
|
101
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cartographer",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
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
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Cartographer plugin are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.6.2](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.6.1...cartographer-v0.6.2) (2026-08-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **cartographer:** drop a typeless finding instead of hashing it as "unknown" :broom: ([#203](https://github.com/onlooker-community/ecosystem/issues/203)) ([a949d5d](https://github.com/onlooker-community/ecosystem/commit/a949d5d43f3ae3515ac6278b35d1be5cc468cb0e))
|
|
11
|
+
|
|
5
12
|
## [0.6.1](https://github.com/onlooker-community/ecosystem/compare/cartographer-v0.6.0...cartographer-v0.6.1) (2026-08-22)
|
|
6
13
|
|
|
7
14
|
|
|
@@ -247,7 +247,29 @@ run_synthesize() {
|
|
|
247
247
|
while IFS= read -r finding; do
|
|
248
248
|
[[ -z "$finding" ]] && continue
|
|
249
249
|
local ftype ffile_a fexcerpt_a ffile_b fexcerpt_b
|
|
250
|
-
|
|
250
|
+
# `// ""` collapses absent, null and empty into one check, the same way
|
|
251
|
+
# cartographer_issue_found_payload does — jq's `//` treats "" as
|
|
252
|
+
# present, so an empty type would otherwise slip past.
|
|
253
|
+
ftype=$(printf '%s' "$finding" | jq -r '.type // ""')
|
|
254
|
+
|
|
255
|
+
# A finding with no type is malformed: every analyzer writes a literal
|
|
256
|
+
# type, so this means the phase that produced it has a bug. Drop it here
|
|
257
|
+
# rather than hashing it.
|
|
258
|
+
#
|
|
259
|
+
# This used to default to "unknown". The emit path already refused to
|
|
260
|
+
# announce such a finding (ecosystem-ci0), but the orchestrator stored
|
|
261
|
+
# it anyway under a hash derived from a type it does not have, and then
|
|
262
|
+
# touched its dedup sentinel. That sentinel made the silence permanent:
|
|
263
|
+
# every later audit saw "known", took the refresh path, and emitted
|
|
264
|
+
# nothing — so the finding sat on disk forever, counted in
|
|
265
|
+
# new_finding_count, and could never be announced. Dropping it keeps the
|
|
266
|
+
# stored corpus honest and leaves the hash of every well-formed finding
|
|
267
|
+
# untouched, so no existing dedup sentinel is orphaned (ecosystem-0ty).
|
|
268
|
+
if [[ -z "$ftype" ]]; then
|
|
269
|
+
log "synthesize: dropped a finding that carries no type — the analyzer that produced it has a bug"
|
|
270
|
+
continue
|
|
271
|
+
fi
|
|
272
|
+
|
|
251
273
|
ffile_a=$(printf '%s' "$finding" | jq -r '.file_a // ""')
|
|
252
274
|
fexcerpt_a=$(printf '%s' "$finding" | jq -r '.excerpt_a // ""')
|
|
253
275
|
ffile_b=$(printf '%s' "$finding" | jq -r '.file_b // ""')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inspector",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Per-edit lint and typecheck gate. Runs the project's configured checks on just the touched file after every Write / Edit / MultiEdit, so the agent sees its own type errors before claiming success. Cheaper than proctor (which runs project-wide verification at Stop); complements assayer (which catches the agent lying about claims). Builds on the Onlooker ecosystem plugin.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.4](https://github.com/onlooker-community/ecosystem/compare/inspector-v0.3.3...inspector-v0.3.4) (2026-08-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **inspector:** canonicalize the file and repo root the same way :mag: ([0f850e9](https://github.com/onlooker-community/ecosystem/commit/0f850e92d7e620e4d7e06f512ef81efe4255f316))
|
|
9
|
+
|
|
3
10
|
## [0.3.3](https://github.com/onlooker-community/ecosystem/compare/inspector-v0.3.2...inspector-v0.3.3) (2026-08-22)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -40,14 +40,10 @@ TOOL_TARGET=$(printf '%s' "$HOOK_INPUT" \
|
|
|
40
40
|
| jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null)
|
|
41
41
|
[[ -z "$TOOL_TARGET" ]] && exit 0
|
|
42
42
|
|
|
43
|
-
# Canonicalize.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
CANONICAL=$(readlink -f "$TOOL_TARGET" 2>/dev/null) || CANONICAL="$TOOL_TARGET"
|
|
48
|
-
else
|
|
49
|
-
CANONICAL="$TOOL_TARGET"
|
|
50
|
-
fi
|
|
43
|
+
# Canonicalize through the same helper the repo root uses. The containment
|
|
44
|
+
# check below is a prefix match, so both sides have to be resolved the same
|
|
45
|
+
# way — see inspector_canonical_path and ecosystem-foi.
|
|
46
|
+
CANONICAL=$(inspector_canonical_path "$TOOL_TARGET")
|
|
51
47
|
export INSPECTOR_FILE="$CANONICAL"
|
|
52
48
|
|
|
53
49
|
REPO_ROOT=$(inspector_project_repo_root "$CWD")
|
|
@@ -23,11 +23,68 @@ _inspector_sha256_first12() {
|
|
|
23
23
|
fi
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
# Resolve a path to its physical form — symlinks expanded, no trailing slash.
|
|
27
|
+
#
|
|
28
|
+
# The hook decides whether a touched file lives inside the repo by prefix-
|
|
29
|
+
# matching one canonicalized path against the other. Both sides MUST come
|
|
30
|
+
# through this one function. They used to be canonicalized independently
|
|
31
|
+
# (realpath for the file, `git rev-parse --show-toplevel` for the root), each
|
|
32
|
+
# with its own fallback, and on macOS /var is a symlink to /private/var — so
|
|
33
|
+
# whenever either mechanism fell back, the two sides landed in different
|
|
34
|
+
# namespaces and the prefix match failed. The hook then reported not_in_repo
|
|
35
|
+
# for a file plainly inside the repo, exiting 0 with no output. See
|
|
36
|
+
# ecosystem-foi, where that surfaced as an intermittent test failure.
|
|
37
|
+
#
|
|
38
|
+
# The last resort is `cd` + `pwd -P`, a bash builtin that needs no external
|
|
39
|
+
# tool, so this still returns a physical path when neither realpath nor a
|
|
40
|
+
# GNU-style `readlink -f` is available.
|
|
41
|
+
#
|
|
42
|
+
# Usage: canonical=$(inspector_canonical_path "$some_path")
|
|
43
|
+
inspector_canonical_path() {
|
|
44
|
+
local p="${1:-}"
|
|
45
|
+
[[ -z "$p" ]] && return 0
|
|
46
|
+
|
|
47
|
+
local out
|
|
48
|
+
if out=$(realpath "$p" 2>/dev/null) && [[ -n "$out" ]]; then
|
|
49
|
+
printf '%s' "$out"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
# BSD readlink has no -f, so this silently no-ops there and falls through.
|
|
53
|
+
if out=$(readlink -f "$p" 2>/dev/null) && [[ -n "$out" ]]; then
|
|
54
|
+
printf '%s' "$out"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ -d "$p" ]]; then
|
|
59
|
+
out=$(cd "$p" 2>/dev/null && pwd -P) && [[ -n "$out" ]] && {
|
|
60
|
+
printf '%s' "$out"
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
local dir base
|
|
65
|
+
dir=$(dirname "$p")
|
|
66
|
+
base=$(basename "$p")
|
|
67
|
+
out=$(cd "$dir" 2>/dev/null && pwd -P) && [[ -n "$out" ]] && {
|
|
68
|
+
printf '%s/%s' "$out" "$base"
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Nothing resolved it — hand back the input rather than an empty string,
|
|
74
|
+
# so a caller comparing two of these still compares like with like.
|
|
75
|
+
printf '%s' "$p"
|
|
76
|
+
}
|
|
77
|
+
|
|
26
78
|
inspector_project_repo_root() {
|
|
27
79
|
local cwd="${1:-$(pwd)}"
|
|
28
80
|
local root
|
|
29
|
-
|
|
30
|
-
|
|
81
|
+
# Canonicalize BOTH the git answer and the fallback. git already returns a
|
|
82
|
+
# physical path, so the first call is a no-op; the fallback is the caller's
|
|
83
|
+
# raw cwd, which is exactly the case that used to diverge.
|
|
84
|
+
root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) \
|
|
85
|
+
&& [[ -n "$root" ]] \
|
|
86
|
+
&& { inspector_canonical_path "$root"; return 0; }
|
|
87
|
+
inspector_canonical_path "$cwd"
|
|
31
88
|
}
|
|
32
89
|
|
|
33
90
|
inspector_project_remote_url() {
|
|
@@ -530,11 +530,32 @@ turn_state_next_tool() {
|
|
|
530
530
|
fi
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
-
# Safely emit
|
|
534
|
-
#
|
|
535
|
-
#
|
|
536
|
-
# to call turn_state_export()
|
|
537
|
-
#
|
|
533
|
+
# Safely emit an Onlooker event through the canonical emitter.
|
|
534
|
+
#
|
|
535
|
+
# Automatically exports turn state if not already set, so a caller does not have
|
|
536
|
+
# to call turn_state_export() itself. Note the turn rides in the PAYLOAD, as
|
|
537
|
+
# turn_number, for the event types whose schema declares it — the envelope has
|
|
538
|
+
# no turn field and is additionalProperties:false.
|
|
539
|
+
#
|
|
540
|
+
# Returns non-zero and writes NOTHING when $ONLOOKER_EMIT is unavailable.
|
|
541
|
+
#
|
|
542
|
+
# There used to be a fallback here that hand-built an envelope with jq and
|
|
543
|
+
# appended it directly. Every line it wrote was unvalidatable: it omitted all
|
|
544
|
+
# five required id/schema_version/runtime/machine_id/sequence fields, and added
|
|
545
|
+
# four the envelope forbids outright (hook_type, tool_name, turn,
|
|
546
|
+
# tool_call_seq). It was reached only when $ONLOOKER_EMIT was missing — a file
|
|
547
|
+
# that ships next to this one, so it never fired in a normal checkout or
|
|
548
|
+
# install — which is how it stayed broken and unnoticed. See ecosystem-0tm.
|
|
549
|
+
#
|
|
550
|
+
# Dropping it rather than teaching it to call the emitter is the same
|
|
551
|
+
# fail-closed choice made for prompt_rules_emit in ecosystem-aaz: an
|
|
552
|
+
# unparseable line on the bus is worse than a missing one, and a second way to
|
|
553
|
+
# build an envelope is a second thing to drift.
|
|
554
|
+
#
|
|
555
|
+
# Callers must tolerate a non-zero return — hooks run under `set -e`, so use
|
|
556
|
+
# `safe_emit ... || true` when the emission is advisory.
|
|
557
|
+
#
|
|
558
|
+
# Usage: safe_emit "event_type" '{"key":"value"}'
|
|
538
559
|
safe_emit() {
|
|
539
560
|
local event_type="$1"
|
|
540
561
|
local payload="$2"
|
|
@@ -544,38 +565,8 @@ safe_emit() {
|
|
|
544
565
|
turn_state_export "$_HOOK_SESSION_ID"
|
|
545
566
|
fi
|
|
546
567
|
|
|
547
|
-
if validate_file_exists "$ONLOOKER_EMIT"; then
|
|
548
|
-
|
|
549
|
-
else
|
|
550
|
-
# Fallback: write directly to events log with envelope enrichment.
|
|
551
|
-
# Uses env vars set by hook_set_context() — NOT stdin (already consumed).
|
|
552
|
-
ensure_file_exists "$ONLOOKER_EVENTS_LOG" || return 1
|
|
553
|
-
local timestamp session_id plugin_name hook_type tool_name turn tool_seq
|
|
554
|
-
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
555
|
-
session_id="${_HOOK_SESSION_ID:-}"
|
|
556
|
-
if [[ -z "$session_id" ]]; then
|
|
557
|
-
session_id=$(echo "$payload" | jq -r '.session_id // "unknown"' 2>/dev/null) || session_id="unknown"
|
|
558
|
-
fi
|
|
559
|
-
plugin_name="${ONLOOKER_PLUGIN_NAME:-unknown}"
|
|
560
|
-
hook_type="${ONLOOKER_HOOK_TYPE:-}"
|
|
561
|
-
tool_name="${ONLOOKER_TOOL_NAME:-}"
|
|
562
|
-
turn="${ONLOOKER_TURN_NUMBER:-}"
|
|
563
|
-
tool_seq="${ONLOOKER_TURN_TOOL_SEQ:-}"
|
|
564
|
-
jq -cn \
|
|
565
|
-
--arg ts "$timestamp" \
|
|
566
|
-
--arg sid "$session_id" \
|
|
567
|
-
--arg plugin "$plugin_name" \
|
|
568
|
-
--arg type "$event_type" \
|
|
569
|
-
--arg hook_type "$hook_type" \
|
|
570
|
-
--arg tool_name "$tool_name" \
|
|
571
|
-
--arg turn "$turn" \
|
|
572
|
-
--arg tool_seq "$tool_seq" \
|
|
573
|
-
--argjson payload "$payload" \
|
|
574
|
-
'{timestamp: $ts, session_id: $sid, plugin: $plugin, event_type: $type, payload: $payload}
|
|
575
|
-
+ (if $hook_type != "" then {hook_type: $hook_type} else {} end)
|
|
576
|
-
+ (if $tool_name != "" then {tool_name: $tool_name} else {} end)
|
|
577
|
-
+ (if $turn != "" then {turn: ($turn | tonumber)} else {} end)
|
|
578
|
-
+ (if $tool_seq != "" then {tool_call_seq: ($tool_seq | tonumber)} else {} end)
|
|
579
|
-
' >> "$ONLOOKER_EVENTS_LOG"
|
|
568
|
+
if ! validate_file_exists "$ONLOOKER_EMIT"; then
|
|
569
|
+
return 1
|
|
580
570
|
fi
|
|
571
|
+
"$ONLOOKER_EMIT" "$event_type" "$payload"
|
|
581
572
|
}
|
|
@@ -268,10 +268,12 @@ _run_audit() {
|
|
|
268
268
|
[ "$output" = "session_start_first_run" ]
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
# A typeless finding must not vanish.
|
|
272
|
-
#
|
|
273
|
-
# the
|
|
274
|
-
#
|
|
271
|
+
# A typeless finding must not vanish. Since ecosystem-0ty the synthesize phase
|
|
272
|
+
# drops it before it is hashed, so this line now comes from there rather than
|
|
273
|
+
# from the emit-phase builder refusal that ecosystem-ci0 added. Both write
|
|
274
|
+
# "carries no type" on purpose: what matters to an operator is that the audit
|
|
275
|
+
# log says a finding was discarded and why, not which layer discarded it. The
|
|
276
|
+
# builder's own refusal is still covered directly in cartographer-events.bats.
|
|
275
277
|
@test "a finding with no type is reported in the audit log" {
|
|
276
278
|
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
277
279
|
#!/usr/bin/env bash
|
|
@@ -286,6 +288,50 @@ STUB
|
|
|
286
288
|
grep -q 'carries no type' "$AUDIT_LOG"
|
|
287
289
|
}
|
|
288
290
|
|
|
291
|
+
# Refusing to announce a typeless finding was only half of ecosystem-ci0. The
|
|
292
|
+
# orchestrator still stored it under a hash computed from a type it does not
|
|
293
|
+
# have, then touched its dedup sentinel — so on every later audit the sentinel
|
|
294
|
+
# said "known", the refresh path ran, and no bus event ever fired again. The
|
|
295
|
+
# finding sat on disk forever, counted in new_finding_count, and was
|
|
296
|
+
# permanently unannounceable. These three assert it is dropped instead
|
|
297
|
+
# (ecosystem-0ty).
|
|
298
|
+
|
|
299
|
+
_typeless_stub() {
|
|
300
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
301
|
+
#!/usr/bin/env bash
|
|
302
|
+
cat >/dev/null
|
|
303
|
+
echo call >> "$CLAUDE_CALL_LOG"
|
|
304
|
+
printf '[{"severity":"warning","file_a":"CLAUDE.md","excerpt_a":"Always prefer tabs.","description":"typeless"}]'
|
|
305
|
+
STUB
|
|
306
|
+
chmod +x "${STUB_BIN}/claude"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@test "a finding with no type is never written to findings/" {
|
|
310
|
+
_typeless_stub
|
|
311
|
+
run _run_audit
|
|
312
|
+
[ "$status" -eq 0 ] || return 1
|
|
313
|
+
[ "$(find "${CARTOGRAPHER_DIR}/findings" -name '*.json' 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@test "a finding with no type leaves no dedup sentinel" {
|
|
317
|
+
# The sentinel is what made this permanent: with one on disk, a corrected
|
|
318
|
+
# finding that later hashes the same way would be seen as already known and
|
|
319
|
+
# silently refreshed instead of announced.
|
|
320
|
+
_typeless_stub
|
|
321
|
+
run _run_audit
|
|
322
|
+
[ "$status" -eq 0 ] || return 1
|
|
323
|
+
[ "$(find "${CARTOGRAPHER_DIR}/dedup" -type f 2>/dev/null | wc -l | tr -d ' ')" = "0" ]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@test "a finding with no type is not counted as a new finding" {
|
|
327
|
+
# audit.complete used to report a finding that produced no issue.found,
|
|
328
|
+
# so a consumer reading only the bus saw counts that its own event stream
|
|
329
|
+
# could not account for.
|
|
330
|
+
_typeless_stub
|
|
331
|
+
_run_audit
|
|
332
|
+
_last_audit_complete | jq -e '.payload.new_finding_count == 0' >/dev/null
|
|
333
|
+
}
|
|
334
|
+
|
|
289
335
|
|
|
290
336
|
# ── Resolution reaching the bus (ecosystem-w2i) ──────────────────────────────
|
|
291
337
|
#
|
|
@@ -155,3 +155,95 @@ EOF
|
|
|
155
155
|
# The last argv slot should now hold the resolved touched file path.
|
|
156
156
|
[[ "$(echo "$argv" | jq -r '.[-1]')" == *"src/sample.ts" ]]
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
# ── path canonicalization (ecosystem-foi) ────────────────────────────────────
|
|
160
|
+
#
|
|
161
|
+
# The hook decides "is this file inside the repo?" by prefix-matching the
|
|
162
|
+
# canonicalized file path against the repo root. Those two paths used to be
|
|
163
|
+
# canonicalized by INDEPENDENT mechanisms — realpath for the file, `git
|
|
164
|
+
# rev-parse --show-toplevel` for the root — each with its own fallback. On
|
|
165
|
+
# macOS /var is a symlink to /private/var, so whenever either mechanism fell
|
|
166
|
+
# back, the two sides landed in different namespaces, the prefix match failed,
|
|
167
|
+
# and the hook reported not_in_repo for a file plainly inside the repo: exit 0,
|
|
168
|
+
# no agent output, one misleading .skipped event.
|
|
169
|
+
#
|
|
170
|
+
# That is the exact signature of the intermittent failure in ecosystem-foi,
|
|
171
|
+
# where the hook exited 0 but emitted no file header. These tests force each
|
|
172
|
+
# fallback deterministically instead of waiting for the flake.
|
|
173
|
+
|
|
174
|
+
_stub_dir() {
|
|
175
|
+
local d="${BATS_TEST_TMPDIR}/stub-$1"
|
|
176
|
+
mkdir -p "$d"
|
|
177
|
+
printf '%s' "$d"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_failing_check_settings() {
|
|
181
|
+
cat <<'EOF' | _settings
|
|
182
|
+
{"inspector":{"checks":{".ts":[
|
|
183
|
+
{"name":"broken","kind":"lint","argv":["sh","-c","echo 'src/sample.ts:1:1 - Bad'; exit 2"]}
|
|
184
|
+
]}}}
|
|
185
|
+
EOF
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@test "the repo-root fallback still finds a file inside the repo when git fails" {
|
|
189
|
+
# git rev-parse failing sends inspector_project_repo_root to its $cwd
|
|
190
|
+
# fallback, which is the caller-supplied (unresolved) path.
|
|
191
|
+
_failing_check_settings
|
|
192
|
+
local stub
|
|
193
|
+
stub=$(_stub_dir git)
|
|
194
|
+
printf '#!/bin/sh\nexit 1\n' >"${stub}/git"
|
|
195
|
+
chmod +x "${stub}/git"
|
|
196
|
+
|
|
197
|
+
run bash -c "printf '%s' '$(_input)' | PATH='${stub}:$PATH' ONLOOKER_DIR='$ONLOOKER_DIR' CLAUDE_PLUGIN_ROOT='$PLUGIN_ROOT' bash '$HOOK'"
|
|
198
|
+
[ "$status" -eq 0 ] || return 1
|
|
199
|
+
[[ "$output" == *"inspector: src/sample.ts"* ]] || return 1
|
|
200
|
+
[ "$(_event_count inspector.check.failed)" = "1" ]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@test "the file is still found inside the repo when realpath is unavailable" {
|
|
204
|
+
# realpath failing leaves the file path unresolved while the repo root,
|
|
205
|
+
# coming from git, is resolved — the same mismatch from the other side.
|
|
206
|
+
_failing_check_settings
|
|
207
|
+
local stub
|
|
208
|
+
stub=$(_stub_dir realpath)
|
|
209
|
+
printf '#!/bin/sh\nexit 1\n' >"${stub}/realpath"
|
|
210
|
+
chmod +x "${stub}/realpath"
|
|
211
|
+
|
|
212
|
+
run bash -c "printf '%s' '$(_input)' | PATH='${stub}:$PATH' ONLOOKER_DIR='$ONLOOKER_DIR' CLAUDE_PLUGIN_ROOT='$PLUGIN_ROOT' bash '$HOOK'"
|
|
213
|
+
[ "$status" -eq 0 ] || return 1
|
|
214
|
+
[[ "$output" == *"inspector: src/sample.ts"* ]] || return 1
|
|
215
|
+
[ "$(_event_count inspector.check.failed)" = "1" ]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@test "a file genuinely outside the repo still reports not_in_repo" {
|
|
219
|
+
# The containment check must still say no when the answer really is no —
|
|
220
|
+
# canonicalizing both sides must not turn it into a rubber stamp.
|
|
221
|
+
_failing_check_settings
|
|
222
|
+
local outside="${BATS_TEST_TMPDIR}/outside.ts"
|
|
223
|
+
printf 'x\n' >"$outside"
|
|
224
|
+
|
|
225
|
+
run _run_hook "$(_input "$REPO" "Edit" "$outside")"
|
|
226
|
+
[ "$status" -eq 0 ] || return 1
|
|
227
|
+
[ -z "$output" ] || return 1
|
|
228
|
+
[ "$(jq -r 'select(.event_type=="inspector.check.skipped").payload.reason' "$ONLOOKER_EVENTS_LOG")" = "not_in_repo" ]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@test "canonicalization survives with neither realpath nor readlink -f" {
|
|
232
|
+
# The last resort is `cd` + `pwd -P`, a builtin. Without it, a box with no
|
|
233
|
+
# GNU coreutils would hit the same namespace mismatch through a third door,
|
|
234
|
+
# and no other test forces this branch.
|
|
235
|
+
_failing_check_settings
|
|
236
|
+
local stub
|
|
237
|
+
stub=$(_stub_dir noresolvers)
|
|
238
|
+
printf '#!/bin/sh\nexit 1\n' >"${stub}/realpath"
|
|
239
|
+
printf '#!/bin/sh\nexit 1\n' >"${stub}/readlink"
|
|
240
|
+
chmod +x "${stub}/realpath" "${stub}/readlink"
|
|
241
|
+
|
|
242
|
+
run bash -c "printf '%s' '$(_input)' | PATH='${stub}:$PATH' ONLOOKER_DIR='$ONLOOKER_DIR' CLAUDE_PLUGIN_ROOT='$PLUGIN_ROOT' bash '$HOOK'"
|
|
243
|
+
[ "$status" -eq 0 ] || return 1
|
|
244
|
+
[[ "$output" == *"inspector: src/sample.ts"* ]] || return 1
|
|
245
|
+
# file_path_relative must still be repo-relative, not an absolute path —
|
|
246
|
+
# that is what proves the prefix strip found the root, rather than the
|
|
247
|
+
# header printing off a coincidentally-passing comparison.
|
|
248
|
+
[ "$(jq -r 'select(.event_type=="inspector.check.failed").payload.file_path_relative' "$ONLOOKER_EVENTS_LOG")" = "src/sample.ts" ]
|
|
249
|
+
}
|
|
@@ -108,6 +108,52 @@ setup() {
|
|
|
108
108
|
>/dev/null
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
@test "safe_emit's line validates as a canonical envelope, not just a lookalike" {
|
|
112
|
+
# The assertion above spot-checks four fields. A hand-built envelope can pass
|
|
113
|
+
# that while still being rejected by the schema, which is exactly how the
|
|
114
|
+
# fallback below stayed broken. Validate the whole line instead.
|
|
115
|
+
export _HOOK_SESSION_ID="emit-session"
|
|
116
|
+
safe_emit "tool.file.read" '{"path":"/tmp/example.txt","read_mode":"full"}'
|
|
117
|
+
tail -n 1 "$ONLOOKER_EVENTS_LOG" \
|
|
118
|
+
| ONLOOKER_DIR="$ONLOOKER_DIR" node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@test "safe_emit writes nothing when the emit script is missing" {
|
|
122
|
+
# The degraded path. It used to hand-build an envelope with jq that omitted
|
|
123
|
+
# every required id/schema_version/runtime/machine_id/sequence field and added
|
|
124
|
+
# four the schema forbids (hook_type, tool_name, turn, tool_call_seq), on an
|
|
125
|
+
# envelope that is additionalProperties:false. An unparseable line on the bus
|
|
126
|
+
# is worse than a missing one, so this path now writes nothing at all.
|
|
127
|
+
# See ecosystem-0tm.
|
|
128
|
+
export _HOOK_SESSION_ID="emit-session"
|
|
129
|
+
export ONLOOKER_HOOK_TYPE="PreToolUse"
|
|
130
|
+
export ONLOOKER_TOOL_NAME="Read"
|
|
131
|
+
export ONLOOKER_TURN_NUMBER=4
|
|
132
|
+
export ONLOOKER_TURN_TOOL_SEQ=2
|
|
133
|
+
export ONLOOKER_EMIT="${BATS_TEST_TMPDIR}/no-such-emit.sh"
|
|
134
|
+
|
|
135
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
136
|
+
run safe_emit "tool.file.read" '{"path":"/tmp/example.txt","read_mode":"full"}'
|
|
137
|
+
[ "$status" -ne 0 ] || return 1
|
|
138
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@test "safe_emit's failure to emit never aborts the calling hook" {
|
|
142
|
+
# safe_emit is substrate: returning non-zero must be absorbable by a caller
|
|
143
|
+
# running under `set -e`, the shape every hook uses. Assert the caller
|
|
144
|
+
# survives and reaches its next statement.
|
|
145
|
+
export ONLOOKER_EMIT="${BATS_TEST_TMPDIR}/no-such-emit.sh"
|
|
146
|
+
run bash -c "
|
|
147
|
+
set -euo pipefail
|
|
148
|
+
source '${REPO_ROOT}/scripts/lib/validate-path.sh'
|
|
149
|
+
export ONLOOKER_EMIT='${BATS_TEST_TMPDIR}/no-such-emit.sh'
|
|
150
|
+
safe_emit 'tool.file.read' '{\"path\":\"/x\",\"read_mode\":\"full\"}' || true
|
|
151
|
+
echo REACHED
|
|
152
|
+
"
|
|
153
|
+
[ "$status" -eq 0 ] || return 1
|
|
154
|
+
[[ "$output" == *"REACHED"* ]]
|
|
155
|
+
}
|
|
156
|
+
|
|
111
157
|
# ----------------------------------------------------------------------------
|
|
112
158
|
# Hook health instrumentation: hook_register / hook_success / hook_failure
|
|
113
159
|
# ----------------------------------------------------------------------------
|