@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.
- package/.claude-plugin/marketplace.json +39 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.github/copilot-instructions.md +46 -0
- package/.github/workflows/coverage.yml +78 -0
- package/.github/workflows/release.yml +24 -8
- package/.github/workflows/test.yml +3 -0
- package/.markdownlintignore +3 -0
- package/.release-please-manifest.json +5 -1
- package/CHANGELOG.md +44 -0
- package/README.md +58 -13
- package/config.json +6 -1
- package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
- package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
- package/docs/adr/003-ulid-over-uuid.md +40 -0
- package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
- package/docs/architecture.md +123 -0
- package/hooks/hooks.json +4 -0
- package/package.json +13 -7
- package/plugins/archivist/.claude-plugin/plugin.json +14 -0
- package/plugins/archivist/CHANGELOG.md +8 -0
- package/plugins/archivist/README.md +105 -0
- package/plugins/archivist/config.json +18 -0
- package/plugins/archivist/hooks/hooks.json +35 -0
- package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
- package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
- package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
- package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
- package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
- package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
- 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/plugins/echo/.claude-plugin/plugin.json +14 -0
- package/plugins/echo/CHANGELOG.md +24 -0
- package/plugins/echo/README.md +110 -0
- package/plugins/echo/config.json +15 -0
- package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
- package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
- package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
- package/plugins/echo/hooks/hooks.json +15 -0
- package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
- package/plugins/echo/scripts/lib/echo-config.sh +108 -0
- package/plugins/echo/scripts/lib/echo-events.sh +74 -0
- package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
- package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
- package/plugins/tribunal/CHANGELOG.md +10 -0
- package/plugins/tribunal/README.md +134 -0
- package/plugins/tribunal/agents/tribunal-actor.md +35 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
- package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
- package/plugins/tribunal/config.json +50 -0
- package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
- package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
- package/plugins/tribunal/hooks/hooks.json +15 -0
- package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
- package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
- package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
- package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
- package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
- package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
- package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
- package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
- package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
- package/release-please-config.json +59 -5
- package/scripts/coverage/bash-coverage.mjs +169 -0
- package/scripts/coverage/format-comment.mjs +120 -0
- package/scripts/coverage/run-coverage.mjs +151 -0
- package/scripts/hooks/agent-spawn-tracker.sh +4 -4
- package/scripts/hooks/prompt-rule-injector.sh +122 -0
- package/scripts/lib/portable-lock.sh +48 -0
- package/scripts/lib/prompt-rules.sh +207 -0
- package/scripts/lib/tool-history.sh +7 -8
- package/scripts/lib/validate-path.sh +4 -0
- package/scripts/lint/check-manifests.mjs +314 -0
- package/scripts/lint/check-references.mjs +311 -0
- package/skills/list-prompt-rules/SKILL.md +15 -0
- package/test/bats/archivist-config-files.bats +60 -0
- package/test/bats/archivist-config.bats +54 -0
- package/test/bats/archivist-inject.bats +73 -0
- package/test/bats/archivist-project-key.bats +75 -0
- package/test/bats/archivist-storage.bats +119 -0
- package/test/bats/archivist-ulid.bats +36 -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
- package/test/bats/config.bats +10 -10
- package/test/bats/echo-config.bats +90 -0
- package/test/bats/echo-events.bats +121 -0
- package/test/bats/echo-project-key.bats +115 -0
- package/test/bats/echo-stop-hook.bats +101 -0
- package/test/bats/echo-ulid.bats +38 -0
- package/test/bats/portable-lock.bats +62 -0
- package/test/bats/prompt-rules.bats +269 -0
- package/test/bats/tribunal-aggregate.bats +77 -0
- package/test/bats/tribunal-config.bats +86 -0
- package/test/bats/tribunal-events.bats +209 -0
- package/test/bats/tribunal-gate.bats +95 -0
- package/test/bats/tribunal-jury.bats +80 -0
- package/test/bats/tribunal-rubric.bats +119 -0
- package/test/bats/tribunal-stop-hook.bats +73 -0
- package/test/bats/tribunal-verdict.bats +71 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
- package/test/helpers/setup.bash +9 -0
- package/test/node/check-manifests.test.mjs +173 -0
- package/test/node/check-references.test.mjs +279 -0
- package/test/node/coverage.test.mjs +143 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/echo-config.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "echo is disabled by default" {
|
|
14
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load ""
|
|
15
|
+
run echo_config_enabled
|
|
16
|
+
[ "$status" -ne 0 ]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "settings.json echo.enabled=true enables echo" {
|
|
20
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
21
|
+
mkdir -p "${repo}/.claude"
|
|
22
|
+
printf '%s\n' '{"echo":{"enabled":true}}' > "${repo}/.claude/settings.json"
|
|
23
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load "$repo"
|
|
24
|
+
run echo_config_enabled
|
|
25
|
+
[ "$status" -eq 0 ]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "settings.json echo.enabled=false overrides plugin default" {
|
|
29
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
30
|
+
mkdir -p "${repo}/.claude"
|
|
31
|
+
printf '%s\n' '{"echo":{"enabled":false}}' > "${repo}/.claude/settings.json"
|
|
32
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load "$repo"
|
|
33
|
+
run echo_config_enabled
|
|
34
|
+
[ "$status" -ne 0 ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "default model is claude-haiku-4-5-20251001" {
|
|
38
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load ""
|
|
39
|
+
local m
|
|
40
|
+
m=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_model)
|
|
41
|
+
[ "$m" = "claude-haiku-4-5-20251001" ]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "default timeout is 60" {
|
|
45
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load ""
|
|
46
|
+
local t
|
|
47
|
+
t=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_timeout)
|
|
48
|
+
[ "$t" = "60" ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "default drift_threshold is 0.05" {
|
|
52
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load ""
|
|
53
|
+
local d
|
|
54
|
+
d=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_drift_threshold)
|
|
55
|
+
[ "$d" = "0.05" ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "default watch_paths includes plugins/*/agents/*.md" {
|
|
59
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load ""
|
|
60
|
+
local paths
|
|
61
|
+
paths=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_watch_paths)
|
|
62
|
+
printf '%s\n' "$paths" | grep -q 'plugins/\*/agents/\*.md'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test "exclude_paths always includes plugins/echo/** regardless of config" {
|
|
66
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load ""
|
|
67
|
+
local excl
|
|
68
|
+
excl=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_exclude_paths)
|
|
69
|
+
printf '%s\n' "$excl" | grep -q 'plugins/echo/\*\*'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "settings.json model override wins over plugin default" {
|
|
73
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
74
|
+
mkdir -p "${repo}/.claude"
|
|
75
|
+
printf '%s\n' '{"echo":{"evaluation":{"model":"claude-opus-4-7"}}}' > "${repo}/.claude/settings.json"
|
|
76
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load "$repo"
|
|
77
|
+
local m
|
|
78
|
+
m=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_model)
|
|
79
|
+
[ "$m" = "claude-opus-4-7" ]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@test "settings.json drift_threshold override wins" {
|
|
83
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
84
|
+
mkdir -p "${repo}/.claude"
|
|
85
|
+
printf '%s\n' '{"echo":{"drift_threshold":0.1}}' > "${repo}/.claude/settings.json"
|
|
86
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_load "$repo"
|
|
87
|
+
local d
|
|
88
|
+
d=$(CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" echo_config_drift_threshold)
|
|
89
|
+
[ "$d" = "0.1" ]
|
|
90
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Validates every emitted echo.* event against @onlooker-community/schema.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
10
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
11
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
12
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
13
|
+
|
|
14
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
15
|
+
export CLAUDE_SESSION_ID="bats-session-$$"
|
|
16
|
+
|
|
17
|
+
# shellcheck disable=SC1091
|
|
18
|
+
source "${PLUGIN_ROOT}/scripts/lib/echo-events.sh"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_validate_latest_event() {
|
|
22
|
+
local last
|
|
23
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
24
|
+
[ -n "$last" ] || return 1
|
|
25
|
+
printf '%s' "$last" | ONLOOKER_DIR="$ONLOOKER_DIR" \
|
|
26
|
+
node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
SUITE_ID="01J000000000000000000000SS"
|
|
30
|
+
TEST_ID="01J000000000000000000000TT"
|
|
31
|
+
|
|
32
|
+
@test "echo.suite.started validates" {
|
|
33
|
+
local p
|
|
34
|
+
p=$(jq -n --arg s "$SUITE_ID" '{
|
|
35
|
+
suite_id: $s,
|
|
36
|
+
test_count: 2,
|
|
37
|
+
trigger: "file_change",
|
|
38
|
+
changed_file: "plugins/tribunal/agents/tribunal-judge-standard.md"
|
|
39
|
+
}')
|
|
40
|
+
echo_emit_event "echo.suite.started" "$p"
|
|
41
|
+
run _validate_latest_event
|
|
42
|
+
[ "$status" -eq 0 ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "echo.suite.complete validates without drift fields" {
|
|
46
|
+
local p
|
|
47
|
+
p=$(jq -n --arg s "$SUITE_ID" '{
|
|
48
|
+
suite_id: $s,
|
|
49
|
+
test_count: 1,
|
|
50
|
+
improved: 0,
|
|
51
|
+
degraded: 0,
|
|
52
|
+
neutral: 1,
|
|
53
|
+
merge_recommended: true,
|
|
54
|
+
duration_ms: 3200
|
|
55
|
+
}')
|
|
56
|
+
echo_emit_event "echo.suite.complete" "$p"
|
|
57
|
+
run _validate_latest_event
|
|
58
|
+
[ "$status" -eq 0 ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "echo.suite.complete validates with drift fields" {
|
|
62
|
+
local p
|
|
63
|
+
p=$(jq -n --arg s "$SUITE_ID" '{
|
|
64
|
+
suite_id: $s,
|
|
65
|
+
test_count: 1,
|
|
66
|
+
improved: 1,
|
|
67
|
+
degraded: 0,
|
|
68
|
+
neutral: 0,
|
|
69
|
+
merge_recommended: true,
|
|
70
|
+
duration_ms: 3200,
|
|
71
|
+
baseline_score: 0.72,
|
|
72
|
+
score_after: 0.85,
|
|
73
|
+
drift: 0.13,
|
|
74
|
+
drift_threshold: 0.05
|
|
75
|
+
}')
|
|
76
|
+
echo_emit_event "echo.suite.complete" "$p"
|
|
77
|
+
run _validate_latest_event
|
|
78
|
+
[ "$status" -eq 0 ]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "echo.improvement.detected validates" {
|
|
82
|
+
local p
|
|
83
|
+
p=$(jq -n --arg s "$SUITE_ID" --arg t "$TEST_ID" '{
|
|
84
|
+
suite_id: $s,
|
|
85
|
+
test_id: $t,
|
|
86
|
+
test_name: "tribunal-judge-standard.md",
|
|
87
|
+
score_before: 0.70,
|
|
88
|
+
score_after: 0.85,
|
|
89
|
+
delta: 0.15,
|
|
90
|
+
confidence: 0.9
|
|
91
|
+
}')
|
|
92
|
+
echo_emit_event "echo.improvement.detected" "$p"
|
|
93
|
+
run _validate_latest_event
|
|
94
|
+
[ "$status" -eq 0 ]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "echo.regression.detected validates" {
|
|
98
|
+
local p
|
|
99
|
+
p=$(jq -n --arg s "$SUITE_ID" --arg t "$TEST_ID" '{
|
|
100
|
+
suite_id: $s,
|
|
101
|
+
test_id: $t,
|
|
102
|
+
test_name: "tribunal-judge-standard.md",
|
|
103
|
+
score_before: 0.85,
|
|
104
|
+
score_after: 0.62,
|
|
105
|
+
delta: -0.23,
|
|
106
|
+
confidence: 0.88
|
|
107
|
+
}')
|
|
108
|
+
echo_emit_event "echo.regression.detected" "$p"
|
|
109
|
+
run _validate_latest_event
|
|
110
|
+
[ "$status" -eq 0 ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@test "emission fails on unknown event type" {
|
|
114
|
+
run echo_emit_event "echo.no.such.event" '{"suite_id":"x"}'
|
|
115
|
+
[ "$status" -ne 0 ]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@test "echo_emit_event returns 1 when payload is empty" {
|
|
119
|
+
run echo_emit_event "echo.suite.started" ""
|
|
120
|
+
[ "$status" -ne 0 ]
|
|
121
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/echo-project-key.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "non-git directory returns empty key" {
|
|
13
|
+
local d="${BATS_TEST_TMPDIR}/non-git"
|
|
14
|
+
mkdir -p "$d"
|
|
15
|
+
run echo_project_key "$d"
|
|
16
|
+
[ "$status" -eq 0 ]
|
|
17
|
+
[ -z "$output" ]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@test "git repo without remote falls back to repo-root hash" {
|
|
21
|
+
local d="${BATS_TEST_TMPDIR}/local-only-repo"
|
|
22
|
+
mkdir -p "$d"
|
|
23
|
+
git -C "$d" init -q
|
|
24
|
+
git -C "$d" config user.email t@example.com
|
|
25
|
+
git -C "$d" config user.name "Test"
|
|
26
|
+
|
|
27
|
+
local k1 k2
|
|
28
|
+
k1=$(echo_project_key "$d")
|
|
29
|
+
k2=$(echo_project_key "$d")
|
|
30
|
+
[ -n "$k1" ]
|
|
31
|
+
[ "${#k1}" -eq 12 ]
|
|
32
|
+
[ "$k1" = "$k2" ]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@test "git repo with remote uses remote hash" {
|
|
36
|
+
local a="${BATS_TEST_TMPDIR}/clone-a"
|
|
37
|
+
local b="${BATS_TEST_TMPDIR}/clone-b"
|
|
38
|
+
mkdir -p "$a" "$b"
|
|
39
|
+
for d in "$a" "$b"; do
|
|
40
|
+
git -C "$d" init -q
|
|
41
|
+
git -C "$d" config user.email t@example.com
|
|
42
|
+
git -C "$d" config user.name "Test"
|
|
43
|
+
git -C "$d" remote add origin git@github.com:org/proj.git
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
local ka kb
|
|
47
|
+
ka=$(echo_project_key "$a")
|
|
48
|
+
kb=$(echo_project_key "$b")
|
|
49
|
+
[ -n "$ka" ]
|
|
50
|
+
[ "$ka" = "$kb" ]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test "different remotes yield different keys" {
|
|
54
|
+
local a="${BATS_TEST_TMPDIR}/proj-a"
|
|
55
|
+
local b="${BATS_TEST_TMPDIR}/proj-b"
|
|
56
|
+
mkdir -p "$a" "$b"
|
|
57
|
+
for d in "$a" "$b"; do
|
|
58
|
+
git -C "$d" init -q
|
|
59
|
+
git -C "$d" config user.email t@example.com
|
|
60
|
+
git -C "$d" config user.name "Test"
|
|
61
|
+
done
|
|
62
|
+
git -C "$a" remote add origin git@github.com:org/proj-a.git
|
|
63
|
+
git -C "$b" remote add origin git@github.com:org/proj-b.git
|
|
64
|
+
|
|
65
|
+
local ka kb
|
|
66
|
+
ka=$(echo_project_key "$a")
|
|
67
|
+
kb=$(echo_project_key "$b")
|
|
68
|
+
[ -n "$ka" ]
|
|
69
|
+
[ -n "$kb" ]
|
|
70
|
+
[ "$ka" != "$kb" ]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "echo_test_id_for_path returns 16 hex chars" {
|
|
74
|
+
local tid
|
|
75
|
+
tid=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
76
|
+
[ -n "$tid" ]
|
|
77
|
+
[ "${#tid}" -eq 16 ]
|
|
78
|
+
[[ "$tid" =~ ^[0-9a-f]{16}$ ]]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "echo_test_id_for_path is stable across calls" {
|
|
82
|
+
local a b
|
|
83
|
+
a=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
84
|
+
b=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
85
|
+
[ "$a" = "$b" ]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@test "echo_test_id_for_path differs for different paths" {
|
|
89
|
+
local a b
|
|
90
|
+
a=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
91
|
+
b=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-adversarial.md")
|
|
92
|
+
[ "$a" != "$b" ]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@test "echo_project_repo_root returns empty for non-git dir" {
|
|
96
|
+
local d="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
97
|
+
mkdir -p "$d"
|
|
98
|
+
local r
|
|
99
|
+
r=$(echo_project_repo_root "$d")
|
|
100
|
+
[ -z "$r" ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "echo_project_repo_root returns repo root for subdir" {
|
|
104
|
+
local d="${BATS_TEST_TMPDIR}/myrepo"
|
|
105
|
+
mkdir -p "${d}/sub/dir"
|
|
106
|
+
git -C "$d" init -q
|
|
107
|
+
git -C "$d" config user.email t@example.com
|
|
108
|
+
git -C "$d" config user.name "Test"
|
|
109
|
+
|
|
110
|
+
local r expected
|
|
111
|
+
r=$(echo_project_repo_root "${d}/sub/dir")
|
|
112
|
+
# Resolve symlinks — on macOS BATS_TEST_TMPDIR may differ from git's toplevel.
|
|
113
|
+
expected=$(cd "$d" && pwd -P)
|
|
114
|
+
[ "$r" = "$expected" ]
|
|
115
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the Echo Stop hook's gating behavior. Does not invoke claude -p
|
|
4
|
+
# (the hook bails before reaching the eval loop when preconditions fail).
|
|
5
|
+
# Tests verify: disabled-by-default, no-git, no-watched-changes, recursion
|
|
6
|
+
# guard, untracked file detection, and stdout silence.
|
|
7
|
+
|
|
8
|
+
setup() {
|
|
9
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
10
|
+
setup_test_env
|
|
11
|
+
|
|
12
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
13
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
14
|
+
HOOK="${PLUGIN_ROOT}/scripts/hooks/echo-stop-gate.sh"
|
|
15
|
+
|
|
16
|
+
REPO="${BATS_TEST_TMPDIR}/repo"
|
|
17
|
+
mkdir -p "$REPO"
|
|
18
|
+
git -C "$REPO" init -q
|
|
19
|
+
git -C "$REPO" config user.email test@example.com
|
|
20
|
+
git -C "$REPO" config user.name test
|
|
21
|
+
(cd "$REPO" && printf 'initial\n' > README.md && git add README.md && git commit -q -m init)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_make_input() {
|
|
25
|
+
local cwd="${1:-$REPO}" sid="${2:-test-session}"
|
|
26
|
+
jq -n --arg cwd "$cwd" --arg sid "$sid" '{cwd: $cwd, session_id: $sid}'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@test "hook exits 0 silently when echo.enabled is false (default)" {
|
|
30
|
+
local input
|
|
31
|
+
input=$(_make_input)
|
|
32
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
33
|
+
[ "$status" -eq 0 ]
|
|
34
|
+
[ -z "$output" ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "hook exits 0 when cwd is not a git repo" {
|
|
38
|
+
local non_repo="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
39
|
+
mkdir -p "$non_repo"
|
|
40
|
+
local input
|
|
41
|
+
input=$(_make_input "$non_repo")
|
|
42
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
43
|
+
[ "$status" -eq 0 ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "hook exits 0 when enabled but no watched files changed" {
|
|
47
|
+
mkdir -p "${REPO}/.claude"
|
|
48
|
+
printf '%s\n' '{"echo":{"enabled":true}}' > "${REPO}/.claude/settings.json"
|
|
49
|
+
local input
|
|
50
|
+
input=$(_make_input)
|
|
51
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
52
|
+
[ "$status" -eq 0 ]
|
|
53
|
+
[ -z "$output" ]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@test "recursion guard: ECHO_NESTED=1 causes immediate exit 0" {
|
|
57
|
+
mkdir -p "${REPO}/.claude"
|
|
58
|
+
printf '%s\n' '{"echo":{"enabled":true}}' > "${REPO}/.claude/settings.json"
|
|
59
|
+
local input
|
|
60
|
+
input=$(_make_input)
|
|
61
|
+
# Export ECHO_NESTED into the subshell that runs the hook.
|
|
62
|
+
run bash -c "printf '%s' '$input' | ECHO_NESTED=1 ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
63
|
+
[ "$status" -eq 0 ]
|
|
64
|
+
[ -z "$output" ]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test "hook never prints to stdout (Stop contract)" {
|
|
68
|
+
local input
|
|
69
|
+
input=$(_make_input)
|
|
70
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
71
|
+
[ "$status" -eq 0 ]
|
|
72
|
+
[ -z "$output" ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "untracked watched file is detected when enabled and claude missing" {
|
|
76
|
+
mkdir -p "${REPO}/.claude" "${REPO}/plugins/myplugin/agents"
|
|
77
|
+
printf '%s\n' '{"echo":{"enabled":true,"watch_paths":["plugins/*/agents/*.md"]}}' \
|
|
78
|
+
> "${REPO}/.claude/settings.json"
|
|
79
|
+
printf '%s\n' '# New agent' > "${REPO}/plugins/myplugin/agents/new-agent.md"
|
|
80
|
+
local input
|
|
81
|
+
input=$(_make_input)
|
|
82
|
+
# claude is not present in the test env PATH → hook reaches _done after the
|
|
83
|
+
# `command -v claude` guard. Exit 0 with no output confirms the file was
|
|
84
|
+
# at least detected (it passed the _done guards before the claude check).
|
|
85
|
+
run bash -c "PATH=/usr/bin:/bin printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
86
|
+
[ "$status" -eq 0 ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@test "files under plugins/echo are excluded even if they match watch_paths" {
|
|
90
|
+
mkdir -p "${REPO}/.claude" "${REPO}/plugins/echo/agents"
|
|
91
|
+
printf '%s\n' '{"echo":{"enabled":true,"watch_paths":["plugins/*/agents/*.md"]}}' \
|
|
92
|
+
> "${REPO}/.claude/settings.json"
|
|
93
|
+
printf '%s\n' '# Echo self' > "${REPO}/plugins/echo/agents/self.md"
|
|
94
|
+
local input
|
|
95
|
+
input=$(_make_input)
|
|
96
|
+
# With echo's own file as the only change, no watched files remain after
|
|
97
|
+
# exclude filtering — hook exits before the claude guard (no output).
|
|
98
|
+
run bash -c "PATH=/usr/bin:/bin printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
99
|
+
[ "$status" -eq 0 ]
|
|
100
|
+
[ -z "$output" ]
|
|
101
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/echo-ulid.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "echo_ulid returns a 26-char crockford base32 string" {
|
|
13
|
+
local id
|
|
14
|
+
id=$(echo_ulid)
|
|
15
|
+
[ "${#id}" -eq 26 ]
|
|
16
|
+
[[ "$id" =~ ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$ ]]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "two ULIDs minted apart are lexicographically ordered or equal" {
|
|
20
|
+
local a b
|
|
21
|
+
a=$(echo_ulid)
|
|
22
|
+
sleep 0.01
|
|
23
|
+
b=$(echo_ulid)
|
|
24
|
+
[[ "$a" < "$b" ]] || [ "$a" = "$b" ]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "many ULIDs are unique" {
|
|
28
|
+
local seen="${BATS_TEST_TMPDIR}/ulids.txt"
|
|
29
|
+
: > "$seen"
|
|
30
|
+
local i
|
|
31
|
+
for ((i = 0; i < 50; i++)); do
|
|
32
|
+
printf '%s\n' "$(echo_ulid)" >> "$seen"
|
|
33
|
+
done
|
|
34
|
+
local total unique
|
|
35
|
+
total=$(wc -l < "$seen" | tr -d ' ')
|
|
36
|
+
unique=$(sort -u "$seen" | wc -l | tr -d ' ')
|
|
37
|
+
[ "$total" = "$unique" ]
|
|
38
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
setup_test_env
|
|
7
|
+
# shellcheck disable=SC1091
|
|
8
|
+
source "${REPO_ROOT}/scripts/lib/portable-lock.sh"
|
|
9
|
+
LOCK="${BATS_TEST_TMPDIR}/test.lock"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "lock_acquire succeeds on an unlocked path" {
|
|
13
|
+
run lock_acquire "$LOCK" 1
|
|
14
|
+
[ "$status" -eq 0 ]
|
|
15
|
+
[ -d "${LOCK}.d" ]
|
|
16
|
+
lock_release "$LOCK"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "lock_acquire on a held lock blocks until released" {
|
|
20
|
+
lock_acquire "$LOCK" 1
|
|
21
|
+
# Start a background releaser after 200ms.
|
|
22
|
+
( sleep 0.2; lock_release "$LOCK" ) &
|
|
23
|
+
local releaser=$!
|
|
24
|
+
# Second acquire should succeed once the releaser fires.
|
|
25
|
+
run lock_acquire "$LOCK" 2
|
|
26
|
+
wait $releaser
|
|
27
|
+
[ "$status" -eq 0 ]
|
|
28
|
+
lock_release "$LOCK"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "lock_acquire returns 1 when timeout elapses with the lock still held" {
|
|
32
|
+
mkdir "${LOCK}.d"
|
|
33
|
+
run lock_acquire "$LOCK" 1
|
|
34
|
+
[ "$status" -eq 1 ]
|
|
35
|
+
rmdir "${LOCK}.d"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "lock_release is a no-op when the lock is not held" {
|
|
39
|
+
run lock_release "$LOCK"
|
|
40
|
+
[ "$status" -eq 0 ]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "concurrent appenders do not interleave writes" {
|
|
44
|
+
local out="${BATS_TEST_TMPDIR}/concurrent.txt"
|
|
45
|
+
: >"$out"
|
|
46
|
+
local n=20
|
|
47
|
+
local i
|
|
48
|
+
for ((i = 0; i < n; i++)); do
|
|
49
|
+
(
|
|
50
|
+
lock_acquire "$LOCK" 5 || exit 1
|
|
51
|
+
# Write a 100-char marker so any byte-level interleave is obvious.
|
|
52
|
+
printf '%s\n' "$(printf 'x%.0s' {1..100})" >>"$out"
|
|
53
|
+
lock_release "$LOCK"
|
|
54
|
+
) &
|
|
55
|
+
done
|
|
56
|
+
wait
|
|
57
|
+
# All lines should be exactly 100 bytes followed by newline.
|
|
58
|
+
local lines
|
|
59
|
+
lines=$(wc -l <"$out" | tr -d ' ')
|
|
60
|
+
[ "$lines" = "$n" ]
|
|
61
|
+
awk 'length($0) != 100 { bad++ } END { exit (bad > 0) }' "$out"
|
|
62
|
+
}
|