@onlooker-community/ecosystem 0.33.1 → 0.34.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/.agents/skills/beads/SKILL.md +80 -0
- package/.agents/skills/beads/agents/openai.yaml +4 -0
- package/.claude/settings.json +13 -0
- package/.claude/skills/writing-tests/SKILL.md +27 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.codex/config.toml +2 -0
- package/.codex/hooks.json +51 -0
- package/.markdownlint.json +3 -0
- package/.release-please-manifest.json +6 -6
- package/AGENTS.md +246 -0
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +56 -1
- package/docs/lesson-promotion-pipeline.md +210 -0
- package/docs/superpowers/plans/2026-08-09-lesson-transform.md +1537 -0
- package/docs/superpowers/specs/2026-08-09-lesson-transform-design.md +261 -0
- package/package.json +3 -2
- package/plugins/assayer/.claude-plugin/plugin.json +1 -1
- package/plugins/assayer/CHANGELOG.md +7 -0
- package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
- package/plugins/curator/.claude-plugin/plugin.json +1 -1
- package/plugins/curator/CHANGELOG.md +7 -0
- package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
- package/plugins/historian/.claude-plugin/plugin.json +1 -1
- package/plugins/historian/CHANGELOG.md +7 -0
- package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
- package/plugins/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +14 -0
- package/plugins/librarian/config.json +4 -0
- package/plugins/librarian/schema/PROVENANCE.json +7 -0
- package/plugins/librarian/schema/lesson-applies-to.subschema.json +74 -0
- package/plugins/librarian/schema/lesson-evidence.subschema.json +36 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +26 -0
- package/plugins/librarian/scripts/lib/librarian-cli.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +135 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +311 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +140 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
- package/plugins/tribunal/CHANGELOG.md +7 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +3 -1
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +2 -1
- package/scripts/lib/prompt-rules.sh +6 -1
- package/scripts/lint/check-lesson-schema-drift.mjs +36 -0
- package/test/bats/archivist-inject.bats +1 -1
- package/test/bats/assayer-extract.bats +2 -2
- package/test/bats/bursar-session-start.bats +3 -3
- package/test/bats/cartographer-lock.bats +3 -3
- package/test/bats/compass-sanitizer.bats +11 -11
- package/test/bats/compass-transcript.bats +2 -2
- package/test/bats/config.bats +15 -15
- package/test/bats/curator-session-start.bats +10 -3
- package/test/bats/emit-payload-default.bats +52 -0
- package/test/bats/governor-ledger.bats +1 -1
- package/test/bats/historian-prompt-submit.bats +1 -1
- package/test/bats/inspector-post-write-hook.bats +4 -4
- package/test/bats/librarian-cli.bats +16 -16
- package/test/bats/librarian-lesson-transform.bats +609 -0
- package/test/bats/librarian-session-start.bats +2 -2
- package/test/bats/lineage-config.bats +1 -1
- package/test/bats/lineage-redact.bats +5 -5
- package/test/bats/session-tracker.bats +4 -4
- package/test/bats/tribunal-jury.bats +1 -1
- package/test/bats/turn-tracker.bats +1 -1
- package/test/bats/warden-sanitizer.bats +3 -3
- package/test/bats/worktree-tracker.bats +2 -2
- package/test/node/lesson-schema-drift.test.mjs +28 -0
- package/test/node/lesson-validate-agreement.test.mjs +154 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Guards the vendored lesson sub-schemas against silent drift.
|
|
3
|
+
//
|
|
4
|
+
// plugins/librarian/schema/*.subschema.json are copied out of the
|
|
5
|
+
// published lesson contract (see PROVENANCE.json) because ajv is
|
|
6
|
+
// unavailable at runtime (ADR-005); the jq rules in
|
|
7
|
+
// librarian-lesson-validate.sh mirror these files by hand. This check
|
|
8
|
+
// only confirms the provenance pin and JSON validity survive edits — it
|
|
9
|
+
// cannot compare against the upstream schema until schema.onlooker.dev
|
|
10
|
+
// publishes lesson schemas.
|
|
11
|
+
//
|
|
12
|
+
// Exit codes:
|
|
13
|
+
// 0 no problems
|
|
14
|
+
// 1 provenance pin changed or a vendored file is missing/invalid
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const dir = 'plugins/librarian/schema';
|
|
19
|
+
let failures = 0;
|
|
20
|
+
const fail = (m) => {
|
|
21
|
+
console.error(`check-lesson-schema: ${m}`);
|
|
22
|
+
failures++;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const prov = JSON.parse(readFileSync(`${dir}/PROVENANCE.json`, 'utf8'));
|
|
27
|
+
if (prov.schema_version !== 2) fail(`expected schema_version 2, got ${prov.schema_version}`);
|
|
28
|
+
for (const f of ['lesson-evidence.subschema.json', 'lesson-applies-to.subschema.json']) {
|
|
29
|
+
JSON.parse(readFileSync(`${dir}/${f}`, 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
fail(err.message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (failures > 0) process.exit(1);
|
|
36
|
+
console.log('check-lesson-schema: ok');
|
|
@@ -46,7 +46,7 @@ setup() {
|
|
|
46
46
|
echo "$output" | jq -e '.hookSpecificOutput.hookEventName == "SessionStart"' >/dev/null
|
|
47
47
|
local ctx
|
|
48
48
|
ctx=$(echo "$output" | jq -r '.hookSpecificOutput.additionalContext')
|
|
49
|
-
[[ "$ctx" == *"use git remote SHA256 as project key"* ]]
|
|
49
|
+
[[ "$ctx" == *"use git remote SHA256 as project key"* ]] || return 1
|
|
50
50
|
[[ "$ctx" == *"Archivist injected 1"* ]]
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -70,7 +70,7 @@ setup() {
|
|
|
70
70
|
@test "extraction prompt includes the message and the JSON contract" {
|
|
71
71
|
run assayer_build_extraction_prompt "I ran the tests and they pass." 5
|
|
72
72
|
[ "$status" -eq 0 ]
|
|
73
|
-
[[ "$output" == *"I ran the tests and they pass."* ]]
|
|
74
|
-
[[ "$output" == *"TESTABLE SUCCESS CLAIM"* ]]
|
|
73
|
+
[[ "$output" == *"I ran the tests and they pass."* ]] || return 1
|
|
74
|
+
[[ "$output" == *"TESTABLE SUCCESS CLAIM"* ]] || return 1
|
|
75
75
|
[[ "$output" == *"at most 5 claims"* ]]
|
|
76
76
|
}
|
|
@@ -68,8 +68,8 @@ _run_hook() {
|
|
|
68
68
|
_seed_ledger
|
|
69
69
|
_run_hook
|
|
70
70
|
[ "$status" -eq 0 ]
|
|
71
|
-
[[ "$output" == *"hookSpecificOutput"* ]]
|
|
72
|
-
[[ "$output" == *"SessionStart"* ]]
|
|
71
|
+
[[ "$output" == *"hookSpecificOutput"* ]] || return 1
|
|
72
|
+
[[ "$output" == *"SessionStart"* ]] || return 1
|
|
73
73
|
[[ "$output" == *"burned \$0.42"* ]]
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -85,7 +85,7 @@ _run_hook() {
|
|
|
85
85
|
> "${dir}/sessions.jsonl"
|
|
86
86
|
_run_hook
|
|
87
87
|
[ "$status" -eq 0 ]
|
|
88
|
-
[[ "$output" == *"burned \$0.00"* ]]
|
|
88
|
+
[[ "$output" == *"burned \$0.00"* ]] || return 1
|
|
89
89
|
[[ "$output" != *"Enable governor"* ]]
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -89,8 +89,8 @@ teardown() {
|
|
|
89
89
|
cartographer_lock_release '${BATS_TEST_TMPDIR}/x.lock' && echo RELEASE_OK
|
|
90
90
|
"
|
|
91
91
|
[ "$status" -eq 0 ]
|
|
92
|
-
[[ "$output" == *"SOURCED_OK"* ]]
|
|
93
|
-
[[ "$output" == *"ACQUIRE_FAILED"* ]]
|
|
94
|
-
[[ "$output" == *"RELEASE_OK"* ]]
|
|
92
|
+
[[ "$output" == *"SOURCED_OK"* ]] || return 1
|
|
93
|
+
[[ "$output" == *"ACQUIRE_FAILED"* ]] || return 1
|
|
94
|
+
[[ "$output" == *"RELEASE_OK"* ]] || return 1
|
|
95
95
|
[[ "$output" == *"locking disabled"* ]]
|
|
96
96
|
}
|
|
@@ -24,27 +24,27 @@ setup() {
|
|
|
24
24
|
@test "prior_assistant_turn delimiter is stripped" {
|
|
25
25
|
local out
|
|
26
26
|
out=$(compass_sanitize "evil <prior_assistant_turn> payload" 240)
|
|
27
|
-
[[ "$out" == *"[STRIPPED]"* ]]
|
|
28
|
-
[[ "$out" == *"payload"* ]]
|
|
27
|
+
[[ "$out" == *"[STRIPPED]"* ]] || return 1
|
|
28
|
+
[[ "$out" == *"payload"* ]] || return 1
|
|
29
29
|
[[ "$out" != *"<prior_assistant_turn>"* ]]
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
@test "all four pair-slot delimiters are stripped" {
|
|
33
33
|
local out
|
|
34
34
|
out=$(compass_sanitize "<context_excerpt>x</context_excerpt><tool_input>y</tool_input>" 240)
|
|
35
|
-
[[ "$out" != *"<context_excerpt>"* ]]
|
|
36
|
-
[[ "$out" != *"</context_excerpt>"* ]]
|
|
37
|
-
[[ "$out" != *"<tool_input>"* ]]
|
|
35
|
+
[[ "$out" != *"<context_excerpt>"* ]] || return 1
|
|
36
|
+
[[ "$out" != *"</context_excerpt>"* ]] || return 1
|
|
37
|
+
[[ "$out" != *"<tool_input>"* ]] || return 1
|
|
38
38
|
[[ "$out" != *"</tool_input>"* ]]
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
@test "non-evaluator delimiters are also stripped" {
|
|
42
42
|
local out
|
|
43
43
|
out=$(compass_sanitize "<<SYS>>x<</SYS>> [INST] y [/INST] <| z" 240)
|
|
44
|
-
[[ "$out" != *"<<SYS>>"* ]]
|
|
45
|
-
[[ "$out" != *"<</SYS>>"* ]]
|
|
46
|
-
[[ "$out" != *"[INST]"* ]]
|
|
47
|
-
[[ "$out" != *"[/INST]"* ]]
|
|
44
|
+
[[ "$out" != *"<<SYS>>"* ]] || return 1
|
|
45
|
+
[[ "$out" != *"<</SYS>>"* ]] || return 1
|
|
46
|
+
[[ "$out" != *"[INST]"* ]] || return 1
|
|
47
|
+
[[ "$out" != *"[/INST]"* ]] || return 1
|
|
48
48
|
[[ "$out" != *"<|"* ]]
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -53,8 +53,8 @@ setup() {
|
|
|
53
53
|
input=$(printf 'a\tb\nc\x00d\x01e')
|
|
54
54
|
local out
|
|
55
55
|
out=$(compass_sanitize "$input" 240)
|
|
56
|
-
[[ "$out" == *"a"* && "$out" == *"b"* && "$out" == *"c"* ]]
|
|
57
|
-
[[ "$out" == *"d"* && "$out" == *"e"* ]]
|
|
56
|
+
[[ "$out" == *"a"* && "$out" == *"b"* && "$out" == *"c"* ]] || return 1
|
|
57
|
+
[[ "$out" == *"d"* && "$out" == *"e"* ]] || return 1
|
|
58
58
|
[ "${out}" = "$(printf 'a\tb\ncde')" ]
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -60,8 +60,8 @@ setup() {
|
|
|
60
60
|
EOF
|
|
61
61
|
local out
|
|
62
62
|
out=$(compass_read_prior_turn "$t" 800)
|
|
63
|
-
[[ "$out" == *"[STRIPPED]"* ]]
|
|
64
|
-
[[ "$out" != *"<prior_assistant_turn>"* ]]
|
|
63
|
+
[[ "$out" == *"[STRIPPED]"* ]] || return 1
|
|
64
|
+
[[ "$out" != *"<prior_assistant_turn>"* ]] || return 1
|
|
65
65
|
|
|
66
66
|
# Truncation honors max_chars.
|
|
67
67
|
local short
|
package/test/bats/config.bats
CHANGED
|
@@ -27,7 +27,7 @@ setup_file() {
|
|
|
27
27
|
|
|
28
28
|
local hook_cmd
|
|
29
29
|
hook_cmd=$(jq -r '.hooks.PreToolUse[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
30
|
-
[[ "$hook_cmd" == *tool-sequence-tracker.sh ]]
|
|
30
|
+
[[ "$hook_cmd" == *tool-sequence-tracker.sh ]] || return 1
|
|
31
31
|
|
|
32
32
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
33
33
|
script_path="${script_path//\"/}"
|
|
@@ -41,7 +41,7 @@ setup_file() {
|
|
|
41
41
|
|
|
42
42
|
local hook_cmd
|
|
43
43
|
hook_cmd=$(jq -r '.hooks.PreToolUse[1].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
44
|
-
[[ "$hook_cmd" == *agent-spawn-tracker.sh ]]
|
|
44
|
+
[[ "$hook_cmd" == *agent-spawn-tracker.sh ]] || return 1
|
|
45
45
|
|
|
46
46
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
47
47
|
script_path="${script_path//\"/}"
|
|
@@ -55,7 +55,7 @@ setup_file() {
|
|
|
55
55
|
|
|
56
56
|
local hook_cmd
|
|
57
57
|
hook_cmd=$(jq -r '.hooks.PreToolUse[2].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
58
|
-
[[ "$hook_cmd" == *skill-usage-tracker.sh ]]
|
|
58
|
+
[[ "$hook_cmd" == *skill-usage-tracker.sh ]] || return 1
|
|
59
59
|
|
|
60
60
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
61
61
|
script_path="${script_path//\"/}"
|
|
@@ -78,7 +78,7 @@ setup_file() {
|
|
|
78
78
|
|
|
79
79
|
local hook_cmd
|
|
80
80
|
hook_cmd=$(jq -r '.hooks.PostToolUse[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
81
|
-
[[ "$hook_cmd" == *tool-history-tracker.sh ]]
|
|
81
|
+
[[ "$hook_cmd" == *tool-history-tracker.sh ]] || return 1
|
|
82
82
|
|
|
83
83
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
84
84
|
script_path="${script_path//\"/}"
|
|
@@ -103,9 +103,9 @@ setup_file() {
|
|
|
103
103
|
turn_cmd=$(jq -r '.hooks.UserPromptSubmit[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
104
104
|
duration_cmd=$(jq -r '.hooks.UserPromptSubmit[0].hooks[1].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
105
105
|
rule_cmd=$(jq -r '.hooks.UserPromptSubmit[0].hooks[2].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
106
|
-
[[ "$turn_cmd" == *turn-tracker.sh ]]
|
|
107
|
-
[[ "$duration_cmd" == *session-duration-tracker.sh ]]
|
|
108
|
-
[[ "$rule_cmd" == *prompt-rule-injector.sh ]]
|
|
106
|
+
[[ "$turn_cmd" == *turn-tracker.sh ]] || return 1
|
|
107
|
+
[[ "$duration_cmd" == *session-duration-tracker.sh ]] || return 1
|
|
108
|
+
[[ "$rule_cmd" == *prompt-rule-injector.sh ]] || return 1
|
|
109
109
|
|
|
110
110
|
for hook_cmd in "$turn_cmd" "$duration_cmd" "$rule_cmd"; do
|
|
111
111
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
@@ -121,7 +121,7 @@ setup_file() {
|
|
|
121
121
|
|
|
122
122
|
local hook_cmd
|
|
123
123
|
hook_cmd=$(jq -r '.hooks.SessionStart[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
124
|
-
[[ "$hook_cmd" == *session-start-tracker.sh ]]
|
|
124
|
+
[[ "$hook_cmd" == *session-start-tracker.sh ]] || return 1
|
|
125
125
|
|
|
126
126
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
127
127
|
script_path="${script_path//\"/}"
|
|
@@ -135,7 +135,7 @@ setup_file() {
|
|
|
135
135
|
|
|
136
136
|
local hook_cmd
|
|
137
137
|
hook_cmd=$(jq -r '.hooks.PreCompact[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
138
|
-
[[ "$hook_cmd" == *pre-compact-tracker.sh ]]
|
|
138
|
+
[[ "$hook_cmd" == *pre-compact-tracker.sh ]] || return 1
|
|
139
139
|
|
|
140
140
|
run jq -e '.hooks.PreCompact[0].matcher == "manual" and .hooks.PreCompact[1].matcher == "auto"' \
|
|
141
141
|
"${REPO_ROOT}/hooks/hooks.json"
|
|
@@ -153,7 +153,7 @@ setup_file() {
|
|
|
153
153
|
|
|
154
154
|
local hook_cmd
|
|
155
155
|
hook_cmd=$(jq -r '.hooks.PostCompact[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
156
|
-
[[ "$hook_cmd" == *context-compact-tracker.sh ]]
|
|
156
|
+
[[ "$hook_cmd" == *context-compact-tracker.sh ]] || return 1
|
|
157
157
|
|
|
158
158
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
159
159
|
script_path="${script_path//\"/}"
|
|
@@ -167,7 +167,7 @@ setup_file() {
|
|
|
167
167
|
|
|
168
168
|
local hook_cmd
|
|
169
169
|
hook_cmd=$(jq -r '.hooks.SessionEnd[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
170
|
-
[[ "$hook_cmd" == *session-end-tracker.sh ]]
|
|
170
|
+
[[ "$hook_cmd" == *session-end-tracker.sh ]] || return 1
|
|
171
171
|
|
|
172
172
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
173
173
|
script_path="${script_path//\"/}"
|
|
@@ -178,7 +178,7 @@ setup_file() {
|
|
|
178
178
|
@test "hooks.json TaskCreated references task-tracker" {
|
|
179
179
|
local hook_cmd
|
|
180
180
|
hook_cmd=$(jq -r '.hooks.TaskCreated[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
181
|
-
[[ "$hook_cmd" == *task-tracker.sh ]]
|
|
181
|
+
[[ "$hook_cmd" == *task-tracker.sh ]] || return 1
|
|
182
182
|
|
|
183
183
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
184
184
|
script_path="${script_path//\"/}"
|
|
@@ -189,7 +189,7 @@ setup_file() {
|
|
|
189
189
|
@test "hooks.json TaskCompleted references task-tracker" {
|
|
190
190
|
local hook_cmd
|
|
191
191
|
hook_cmd=$(jq -r '.hooks.TaskCompleted[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
192
|
-
[[ "$hook_cmd" == *task-tracker.sh ]]
|
|
192
|
+
[[ "$hook_cmd" == *task-tracker.sh ]] || return 1
|
|
193
193
|
|
|
194
194
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
195
195
|
script_path="${script_path//\"/}"
|
|
@@ -200,7 +200,7 @@ setup_file() {
|
|
|
200
200
|
@test "hooks.json WorktreeCreate references worktree-tracker" {
|
|
201
201
|
local hook_cmd
|
|
202
202
|
hook_cmd=$(jq -r '.hooks.WorktreeCreate[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
203
|
-
[[ "$hook_cmd" == *worktree-tracker.sh ]]
|
|
203
|
+
[[ "$hook_cmd" == *worktree-tracker.sh ]] || return 1
|
|
204
204
|
|
|
205
205
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
206
206
|
script_path="${script_path//\"/}"
|
|
@@ -211,7 +211,7 @@ setup_file() {
|
|
|
211
211
|
@test "hooks.json WorktreeRemove references worktree-tracker" {
|
|
212
212
|
local hook_cmd
|
|
213
213
|
hook_cmd=$(jq -r '.hooks.WorktreeRemove[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
214
|
-
[[ "$hook_cmd" == *worktree-tracker.sh ]]
|
|
214
|
+
[[ "$hook_cmd" == *worktree-tracker.sh ]] || return 1
|
|
215
215
|
|
|
216
216
|
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
217
217
|
script_path="${script_path//\"/}"
|
|
@@ -35,10 +35,17 @@ setup() {
|
|
|
35
35
|
# project settings file.
|
|
36
36
|
MEM_DIR="${TEST_HOME}/.claude/projects/test-project/memory"
|
|
37
37
|
mkdir -p "$MEM_DIR" "${PROJECT_REPO}/.claude"
|
|
38
|
+
# Pin a generous wall-clock budget. The hook checks
|
|
39
|
+
# cheap_checks.wall_clock_budget_ms before EACH phase and skips the rest
|
|
40
|
+
# once it trips, so at the shipped default of 500ms a loaded machine can
|
|
41
|
+
# blow the budget before the later phases run — the orphan check among
|
|
42
|
+
# them — and those tests fail intermittently while passing in isolation.
|
|
43
|
+
# These tests assert what the checks detect, not how fast the host is.
|
|
38
44
|
jq -n --arg path "$MEM_DIR" \
|
|
39
45
|
'{
|
|
40
46
|
curator: {
|
|
41
47
|
memory_store_path: $path,
|
|
48
|
+
cheap_checks: { wall_clock_budget_ms: 600000 },
|
|
42
49
|
date_check: { date_grace_period_days: 7 }
|
|
43
50
|
}
|
|
44
51
|
}' > "${PROJECT_REPO}/.claude/settings.json"
|
|
@@ -93,8 +100,8 @@ _write_index() {
|
|
|
93
100
|
# Surfacer rendered the pointer.
|
|
94
101
|
local ctx
|
|
95
102
|
ctx=$(echo "$output" | jq -r '.hookSpecificOutput.additionalContext')
|
|
96
|
-
[[ "$ctx" == *"Curator: 1 open finding"* ]]
|
|
97
|
-
[[ "$ctx" == *"date-decayed"* ]]
|
|
103
|
+
[[ "$ctx" == *"Curator: 1 open finding"* ]] || return 1
|
|
104
|
+
[[ "$ctx" == *"date-decayed"* ]] || return 1
|
|
98
105
|
[[ "$ctx" == *"/curator review"* ]]
|
|
99
106
|
}
|
|
100
107
|
|
|
@@ -256,7 +263,7 @@ BODY
|
|
|
256
263
|
[ "$status" -eq 0 ]
|
|
257
264
|
local ctx
|
|
258
265
|
ctx=$(echo "$output" | jq -r '.hookSpecificOutput.additionalContext')
|
|
259
|
-
[[ "$ctx" == *"2 date-decayed"* ]]
|
|
266
|
+
[[ "$ctx" == *"2 date-decayed"* ]] || return 1
|
|
260
267
|
# The buggy output would have looked like "1 date-decayed, 1 date-decayed".
|
|
261
268
|
[[ "$ctx" != *"1 date-decayed, 1 date-decayed"* ]]
|
|
262
269
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
#
|
|
3
|
+
# Guards the payload-default expansion in every emit helper.
|
|
4
|
+
#
|
|
5
|
+
# Two tempting forms are both wrong, and each fails in a way the other does not:
|
|
6
|
+
#
|
|
7
|
+
# "${3:-{\}}" keeps the backslash on bash 3.2 (the bash macOS ships, and the
|
|
8
|
+
# one `#!/usr/bin/env bash` resolves to there), yielding {\} —
|
|
9
|
+
# invalid JSON, so the emit silently drops the event. Works on
|
|
10
|
+
# bash 5.x, which is why CI never caught it.
|
|
11
|
+
# "${3:-{}}" appends a stray `}` to any payload that IS supplied, on EVERY
|
|
12
|
+
# bash version, corrupting the far more common path.
|
|
13
|
+
#
|
|
14
|
+
# The brace-free form below is correct on both. This test runs the real
|
|
15
|
+
# expansion under the real interpreter rather than asserting on source text.
|
|
16
|
+
|
|
17
|
+
setup() {
|
|
18
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
19
|
+
setup_test_env
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_emit_libs() {
|
|
23
|
+
printf '%s\n' \
|
|
24
|
+
"scripts/lib/prompt-rules.sh" \
|
|
25
|
+
"plugins/curator/scripts/lib/curator-emit.sh" \
|
|
26
|
+
"plugins/historian/scripts/lib/historian-emit.sh" \
|
|
27
|
+
"plugins/librarian/scripts/lib/librarian-emit.sh" \
|
|
28
|
+
"plugins/librarian/scripts/lib/librarian-cli.sh" \
|
|
29
|
+
"plugins/tribunal/scripts/lib/tribunal-gate.sh"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@test "no emit helper inlines braces in a parameter-expansion default" {
|
|
33
|
+
# Code only — a comment explaining the hazard must not trip its own guard.
|
|
34
|
+
local hits
|
|
35
|
+
hits=$(cd "$REPO_ROOT" && grep -rn ':-{' --include="*.sh" scripts/ plugins/ \
|
|
36
|
+
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' || true)
|
|
37
|
+
[ -z "$hits" ]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@test "the brace-free default yields {} when no payload is passed" {
|
|
41
|
+
local probe="${BATS_TEST_TMPDIR}/probe.sh"
|
|
42
|
+
printf '%s\n' 'f() { local p="${3:-}"; [ -z "$p" ] && p='"'"'{}'"'"'; printf "%s" "$p"; }' 'f a b' >"$probe"
|
|
43
|
+
run bash "$probe"
|
|
44
|
+
[ "$status" -eq 0 ] && [ "$output" = '{}' ]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@test "the brace-free default passes a supplied payload through unchanged" {
|
|
48
|
+
local probe="${BATS_TEST_TMPDIR}/probe.sh"
|
|
49
|
+
printf '%s\n' 'f() { local p="${3:-}"; [ -z "$p" ] && p='"'"'{}'"'"'; printf "%s" "$p"; }' 'f a b '"'"'{"a":{"b":1}}'"'"'' >"$probe"
|
|
50
|
+
run bash "$probe"
|
|
51
|
+
[ "$status" -eq 0 ] && [ "$output" = '{"a":{"b":1}}' ]
|
|
52
|
+
}
|
|
@@ -157,7 +157,7 @@ _index_session() {
|
|
|
157
157
|
|
|
158
158
|
local ctx
|
|
159
159
|
ctx=$(echo "$output" | jq -r '.hookSpecificOutput.additionalContext')
|
|
160
|
-
[[ "$ctx" == *"Historian: a past chunk looks similar"* ]]
|
|
160
|
+
[[ "$ctx" == *"Historian: a past chunk looks similar"* ]] || return 1
|
|
161
161
|
[[ "$ctx" == *"redash"* ]]
|
|
162
162
|
|
|
163
163
|
grep '"event_type":"historian.retrieval.surfaced"' "$ONLOOKER_EVENTS_LOG" \
|
|
@@ -100,7 +100,7 @@ _run_hook() {
|
|
|
100
100
|
echo '{"inspector":{"show_clean_runs":true,"checks":{".ts":[{"name":"clean","kind":"lint","argv":["true"]}]}}}' | _settings
|
|
101
101
|
run _run_hook "$(_input)"
|
|
102
102
|
[ "$status" -eq 0 ]
|
|
103
|
-
[[ "$output" == *"inspector: src/sample.ts"* ]]
|
|
103
|
+
[[ "$output" == *"inspector: src/sample.ts"* ]] || return 1
|
|
104
104
|
[[ "$output" == *"✓ clean"* ]]
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -112,9 +112,9 @@ _run_hook() {
|
|
|
112
112
|
EOF
|
|
113
113
|
run _run_hook "$(_input)"
|
|
114
114
|
[ "$status" -eq 0 ]
|
|
115
|
-
[[ "$output" == *"inspector: src/sample.ts"* ]]
|
|
116
|
-
[[ "$output" == *"✗ broken"* ]]
|
|
117
|
-
[[ "$output" == *"src/sample.ts:1:1 - Bad"* ]]
|
|
115
|
+
[[ "$output" == *"inspector: src/sample.ts"* ]] || return 1
|
|
116
|
+
[[ "$output" == *"✗ broken"* ]] || return 1
|
|
117
|
+
[[ "$output" == *"src/sample.ts:1:1 - Bad"* ]] || return 1
|
|
118
118
|
[ "$(_event_count inspector.check.failed)" = "1" ]
|
|
119
119
|
[ "$(jq -r 'select(.event_type=="inspector.check.failed").payload.exit_code' "$ONLOOKER_EVENTS_LOG")" = "2" ]
|
|
120
120
|
}
|
|
@@ -124,13 +124,13 @@ _seed_proposal() {
|
|
|
124
124
|
run librarian_cli_list "$PROJECT_REPO"
|
|
125
125
|
[ "$status" -eq 0 ]
|
|
126
126
|
# Header counts only pending entries (2 of 3).
|
|
127
|
-
[[ "$output" == *"2 pending proposal"* ]]
|
|
127
|
+
[[ "$output" == *"2 pending proposal"* ]] || return 1
|
|
128
128
|
# Both pending titles surface.
|
|
129
|
-
[[ "$output" == *"Prefer functional patterns"* ]]
|
|
130
|
-
[[ "$output" == *"Auth rewrite is compliance"* ]]
|
|
129
|
+
[[ "$output" == *"Prefer functional patterns"* ]] || return 1
|
|
130
|
+
[[ "$output" == *"Auth rewrite is compliance"* ]] || return 1
|
|
131
131
|
# Pending rows include full IDs that can be used with show/accept/reject/defer.
|
|
132
|
-
[[ "$output" == *"01LISTPENDINGA000000000000"* ]]
|
|
133
|
-
[[ "$output" == *"01LISTPENDINGB000000000000"* ]]
|
|
132
|
+
[[ "$output" == *"01LISTPENDINGA000000000000"* ]] || return 1
|
|
133
|
+
[[ "$output" == *"01LISTPENDINGB000000000000"* ]] || return 1
|
|
134
134
|
# Accepted entry does NOT appear in the list output.
|
|
135
135
|
[[ "$output" != *"Already accepted"* ]]
|
|
136
136
|
}
|
|
@@ -142,10 +142,10 @@ _seed_proposal() {
|
|
|
142
142
|
|
|
143
143
|
run librarian_cli_show "01SHOWPROPOSAL0000000000000" "$PROJECT_REPO"
|
|
144
144
|
[ "$status" -eq 0 ]
|
|
145
|
-
[[ "$output" == *"01SHOWPROPOSAL0000000000000"* ]]
|
|
146
|
-
[[ "$output" == *"type: feedback"* ]]
|
|
147
|
-
[[ "$output" == *"filename: feedback_x.md"* ]]
|
|
148
|
-
[[ "$output" == *"classifier_confidence: 0.82"* ]]
|
|
145
|
+
[[ "$output" == *"01SHOWPROPOSAL0000000000000"* ]] || return 1
|
|
146
|
+
[[ "$output" == *"type: feedback"* ]] || return 1
|
|
147
|
+
[[ "$output" == *"filename: feedback_x.md"* ]] || return 1
|
|
148
|
+
[[ "$output" == *"classifier_confidence: 0.82"* ]] || return 1
|
|
149
149
|
[[ "$output" == *"Body of the memory."* ]]
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -165,7 +165,7 @@ _seed_proposal() {
|
|
|
165
165
|
|
|
166
166
|
run librarian_cli_accept "01ACCEPTPROPOSAL000000000000" "$PROJECT_REPO"
|
|
167
167
|
[ "$status" -eq 0 ]
|
|
168
|
-
[[ "$output" == *"Accepted."* ]]
|
|
168
|
+
[[ "$output" == *"Accepted."* ]] || return 1
|
|
169
169
|
|
|
170
170
|
local out_file="${MEM_DIR}/feedback_functional.md"
|
|
171
171
|
[ -f "$out_file" ]
|
|
@@ -220,7 +220,7 @@ _seed_proposal() {
|
|
|
220
220
|
|
|
221
221
|
run librarian_cli_accept "01ACCEPTUNSAFE00000000000000" "$PROJECT_REPO"
|
|
222
222
|
[ "$status" -eq 1 ]
|
|
223
|
-
[[ "$output" == *"Failed to write memory file."* ]]
|
|
223
|
+
[[ "$output" == *"Failed to write memory file."* ]] || return 1
|
|
224
224
|
|
|
225
225
|
# No memory file written anywhere under MEM_DIR or its parent.
|
|
226
226
|
[ ! -f "${MEM_DIR}/../escape.md" ]
|
|
@@ -238,8 +238,8 @@ _seed_proposal() {
|
|
|
238
238
|
|
|
239
239
|
run librarian_cli_reject "01REJECTPROPOSAL00000000000" "stale guidance" "$PROJECT_REPO"
|
|
240
240
|
[ "$status" -eq 0 ]
|
|
241
|
-
[[ "$output" == *"Rejected"* ]]
|
|
242
|
-
[[ "$output" == *"stale guidance"* ]]
|
|
241
|
+
[[ "$output" == *"Rejected"* ]] || return 1
|
|
242
|
+
[[ "$output" == *"stale guidance"* ]] || return 1
|
|
243
243
|
|
|
244
244
|
# Proposal status now rejected with the reason captured.
|
|
245
245
|
local proposal_path="${LIBRARIAN_DIR}/proposals/01REJECTPROPOSAL00000000000.json"
|
|
@@ -268,7 +268,7 @@ _seed_proposal() {
|
|
|
268
268
|
|
|
269
269
|
run librarian_cli_defer "01DEFERPROPOSAL000000000000" "$PROJECT_REPO"
|
|
270
270
|
[ "$status" -eq 0 ]
|
|
271
|
-
[[ "$output" == *"Deferred"* ]]
|
|
271
|
+
[[ "$output" == *"Deferred"* ]] || return 1
|
|
272
272
|
|
|
273
273
|
local proposal_path="${LIBRARIAN_DIR}/proposals/01DEFERPROPOSAL000000000000.json"
|
|
274
274
|
jq -e '.status == "pending"' "$proposal_path" >/dev/null
|
|
@@ -285,8 +285,8 @@ _seed_proposal() {
|
|
|
285
285
|
|
|
286
286
|
run librarian_cli_status "$PROJECT_REPO"
|
|
287
287
|
[ "$status" -eq 0 ]
|
|
288
|
-
[[ "$output" == *"pending: 1"* ]]
|
|
289
|
-
[[ "$output" == *"accepted: 1"* ]]
|
|
288
|
+
[[ "$output" == *"pending: 1"* ]] || return 1
|
|
289
|
+
[[ "$output" == *"accepted: 1"* ]] || return 1
|
|
290
290
|
[[ "$output" == *"rejected: 1"* ]]
|
|
291
291
|
}
|
|
292
292
|
|