fix(kyberforge): resolve PR #144 review and audit round 3
- factory-audit: hook events judged per deployed target after apm's rename (Claude/Copilot event sets FAIL, others SUGGESTION); Claude plugin layouts accepted as hook sources; interpreter options and sh -c strings checked; bats 378 -> 386 - primitive-author: Must 4/5 match the audit; reference hand-back points at the right steps; Step 4.2 --target all fallback - skill-author: new-skill.sh repair only on the template marker line, so complete skills stay a no-op; provenance and calibration text - forge: restore "already named" qualifier; drop false HITL claim - apm-workflow: token example uses an env var - docs/hooks.md: the apm-hooks.json sidecar is committed, not ignored Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
@@ -95,36 +95,72 @@ teardown() {
|
||||
assert_output --partial "naked settings-slice shape"
|
||||
}
|
||||
|
||||
@test "hook: an all-lowercase event no target maps is a FAIL" {
|
||||
@test "hook: an all-lowercase event no target fires is a FAIL naming the targets" {
|
||||
write_hook hooks.json '{"hooks":{"pretooluse":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'pretooluse' is all-lowercase — no target this package deploys to maps it"
|
||||
assert_output --partial "event 'pretooluse' never fires on claude, copilot"
|
||||
}
|
||||
|
||||
@test "hook: lowercase stop is a SUGGESTION for every target, clean for Kiro only, a FAIL without Kiro" {
|
||||
@test "hook: lowercase stop with no targets: is a FAIL — Claude and Copilot never fire it" {
|
||||
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
assert_output --partial "SUGGESTION event 'stop' is all-lowercase — only Kiro renames it"
|
||||
|
||||
printf 'name: test-package\nversion: 0.1.0\ntargets: [kiro]\n' > "$PKG/apm.yml"
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "all-lowercase"
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'stop' never fires on claude, copilot"
|
||||
|
||||
printf 'name: test-package\nversion: 0.1.0\ntargets: [claude, copilot]\n' > "$PKG/apm.yml"
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'stop' is all-lowercase — no target this package deploys to maps it"
|
||||
assert_output --partial "event 'stop' never fires on claude, copilot"
|
||||
}
|
||||
|
||||
@test "hook: camelCase userPromptSubmit in a Claude-shaped file is a FAIL; mapped sessionStart is not" {
|
||||
@test "hook: lowercase stop is clean for Kiro only (Kiro renames it to Stop)" {
|
||||
printf 'name: test-package\nversion: 0.1.0\ntargets: [kiro]\n' > "$PKG/apm.yml"
|
||||
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "event 'stop'"
|
||||
}
|
||||
|
||||
@test "hook: lowercase stop in a Cursor-only package passes, at most a SUGGESTION (no published Cursor event list)" {
|
||||
printf 'name: test-package\nversion: 0.1.0\ntargets: [cursor]\n' > "$PKG/apm.yml"
|
||||
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "FAIL"
|
||||
assert_output --partial "SUGGESTION event 'stop' reaches cursor verbatim"
|
||||
}
|
||||
|
||||
@test "hook: a Windsurf-only snake_case event is a SUGGESTION, not a FAIL" {
|
||||
printf 'name: test-package\nversion: 0.1.0\ntargets: [windsurf]\n' > "$PKG/apm.yml"
|
||||
write_hook hooks.json '{"hooks":{"pre_run_command":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
assert_output --partial "SUGGESTION event 'pre_run_command' reaches windsurf verbatim"
|
||||
}
|
||||
|
||||
@test "hook: a misspelled PascalCase event targeting Claude is a FAIL" {
|
||||
printf 'name: test-package\nversion: 0.1.0\ntargets: [claude]\n' > "$PKG/apm.yml"
|
||||
write_hook hooks.json '{"hooks":{"PreToolUSe":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'PreToolUSe' never fires on claude"
|
||||
}
|
||||
|
||||
@test "hook: a real Claude event apm does not rename for Copilot FAILs on Copilot only when Copilot does not fire it" {
|
||||
write_hook hooks.json '{"hooks":{"SubagentStop":[{"hooks":[{"type":"command","command":"true"}]}],"PostToolBatch":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
refute_output --partial "event 'SubagentStop'"
|
||||
assert_output --partial "event 'PostToolBatch' never fires on copilot"
|
||||
}
|
||||
|
||||
@test "hook: camelCase userPromptSubmit is a FAIL on Claude; mapped sessionStart is not" {
|
||||
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"hooks":[{"type":"command","command":"true"}]}],"sessionStart":[{"hooks":[{"type":"command","command":"true"}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'userPromptSubmit' is camelCase"
|
||||
refute_output --partial "event 'sessionStart'"
|
||||
assert_output --partial "event 'userPromptSubmit' never fires on claude"
|
||||
refute_output --partial "FAIL event 'sessionStart'"
|
||||
}
|
||||
|
||||
@test "hook: a flat Copilot-shaped file may use camelCase events when the package does not target Claude" {
|
||||
@@ -132,7 +168,7 @@ teardown() {
|
||||
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "camelCase"
|
||||
refute_output --partial "never fires"
|
||||
}
|
||||
|
||||
@test "hook: camelCase in a flat file is a FAIL when the package targets Claude" {
|
||||
@@ -140,17 +176,17 @@ teardown() {
|
||||
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'userPromptSubmit' is camelCase and the package's apm.yml targets Claude"
|
||||
assert_output --partial "event 'userPromptSubmit' never fires on claude —"
|
||||
}
|
||||
|
||||
@test "hook: camelCase in a flat file is a FAIL when apm.yml declares no targets (every target)" {
|
||||
write_hook hooks.json '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "event 'userPromptSubmit' is camelCase"
|
||||
assert_output --partial "event 'userPromptSubmit' never fires on claude"
|
||||
}
|
||||
|
||||
@test "hook: targets are read from the nearest apm.yml walking up, and target: all counts as Claude" {
|
||||
@test "hook: targets are read from the package root's apm.yml, and target: all counts as Claude" {
|
||||
mkdir -p "$PKG/sub/hooks"
|
||||
printf 'name: sub\nversion: 0.1.0\ntarget: copilot\n' > "$PKG/sub/apm.yml"
|
||||
printf '%s\n' '{"hooks":{"userPromptSubmit":[{"type":"command","bash":"true","timeoutSec":5}]}}' > "$PKG/sub/hooks/hooks.json"
|
||||
@@ -160,7 +196,7 @@ teardown() {
|
||||
printf 'name: sub\nversion: 0.1.0\ntarget: all\n' > "$PKG/sub/apm.yml"
|
||||
run bash "$SCRIPT" "$PKG/sub/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "is camelCase"
|
||||
assert_output --partial "never fires on claude"
|
||||
}
|
||||
|
||||
@test "hook: a referenced script that does not exist is a FAIL" {
|
||||
@@ -443,6 +479,62 @@ teardown() {
|
||||
assert_output --partial "is no package source"
|
||||
}
|
||||
|
||||
@test "hook: a Claude-plugin layout (hooks/ beside .claude-plugin/plugin.json, no apm.yml) is package source" {
|
||||
mkdir -p "$TMPDIR/cplug/.claude-plugin" "$TMPDIR/cplug/hooks" "$TMPDIR/cplug/scripts"
|
||||
printf '{"name":"cplug"}\n' > "$TMPDIR/cplug/.claude-plugin/plugin.json"
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' > "$TMPDIR/cplug/scripts/ok.sh"
|
||||
chmod +x "$TMPDIR/cplug/scripts/ok.sh"
|
||||
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/scripts/ok.sh","timeout":5}]}]}}' > "$TMPDIR/cplug/hooks/hooks.json"
|
||||
run bash "$SCRIPT" "$TMPDIR/cplug/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "FAIL"
|
||||
|
||||
rm -rf "$TMPDIR/cplug/.claude-plugin"
|
||||
printf '{"name":"cplug"}\n' > "$TMPDIR/cplug/plugin.json"
|
||||
run bash "$SCRIPT" "$TMPDIR/cplug/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "no package source"
|
||||
}
|
||||
|
||||
@test "hook: interpreter options are skipped to reach the script (bash -e, python3 -u)" {
|
||||
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash -e scripts/check.sh","timeout":5}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "script 'scripts/check.sh' is a bare relative path"
|
||||
|
||||
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"python3 -u /opt/hook.py","timeout":5}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "script '/opt/hook.py' is an absolute path"
|
||||
|
||||
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash -e ./gone.sh","timeout":5}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "script 'gone.sh' does not exist"
|
||||
}
|
||||
|
||||
@test "hook: sh -c checks the first token of its command string; inline code (python3 -c) is not a script" {
|
||||
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"sh -c scripts/check.sh","timeout":5}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "script 'scripts/check.sh' is a bare relative path"
|
||||
|
||||
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"sh -c \"/opt/x.sh --flag\"","timeout":5}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_failure 1
|
||||
assert_output --partial "script '/opt/x.sh' is an absolute path"
|
||||
|
||||
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"python3 -c \"import sys; sys.exit(0)\"","timeout":5}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "FAIL"
|
||||
|
||||
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash -e \"${PLUGIN_ROOT}/.apm/hooks/scripts/check.sh\"","timeout":5}]}]}}'
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
assert_success
|
||||
refute_output --partial "FAIL"
|
||||
}
|
||||
|
||||
@test "hook: an unedited primitive-author hook template is an unfilled-placeholder FAIL" {
|
||||
cp "$REPO_ROOT/plugins/kyberforge/.apm/skills/primitive-author/assets/templates/hook.json.template" "$PKG/.apm/hooks/hooks.json"
|
||||
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
|
||||
|
||||
Reference in New Issue
Block a user