fix(kyberforge): resolve PR #144 review and audit round 1

- factory-audit: no-op hooks, ./ after interpreters, split-quote and
  spaced ${PLUGIN_ROOT} paths, camelCase events in Claude-targeted flat
  files, case-insensitive routing stems, and non-string YAML keys are
  now caught; input: forms and prompt boundary clauses align with
  primitive-author; bats 347 -> 367
- primitive-author: routing forms, quoting guidance, install exit on
  hidden Unicode, argument-hint exception
- forge: drop duplicated gotcha, fit description and body budgets (#143)
- skill-author: primitive-author boundary, Claude-only env vars
- hook: exit unless CLAUDE_PROJECT_DIR is set, so Copilot/Codex never
  run apm update; ADR-0019 correction, ADR-0025 amendment, docs fixes

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:
2026-09-28 20:13:43 +00:00
parent b2d77b2945
commit df28351d3e
23 changed files with 591 additions and 147 deletions

View File

@@ -57,7 +57,7 @@ teardown() {
mkdir -p "$PKG/.apm/agents"
printf -- '---\ndescription: x\n---\n\nbody\n' > "$PKG/.apm/agents/x.instructions.md"
run bash "$SCRIPT" "$PKG/.apm/agents/x.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "is not directly in a .apm/instructions/ directory"
refute_output --partial "counterpart"
}
@@ -77,49 +77,79 @@ teardown() {
@test "hook: invalid JSON is a FAIL" {
write_hook hooks.json '{"hooks": {'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "is not valid JSON"
}
@test "hook: an event value that is not a list is a FAIL" {
write_hook hooks.json '{"hooks":{"PreToolUse":{"hooks":[]}}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "event 'PreToolUse' is not a list"
}
@test "hook: a naked slice with a stray scalar key is a FAIL" {
write_hook hooks.json '{"description":"x","PreToolUse":[{"hooks":[{"type":"command","command":"true"}]}]}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "naked settings-slice shape"
}
@test "hook: an all-lowercase event is a FAIL" {
write_hook hooks.json '{"hooks":{"stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "event 'stop' is all-lowercase"
}
@test "hook: camelCase userPromptSubmit in a Claude-shaped file is a FAIL; 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
assert_failure 1
assert_output --partial "event 'userPromptSubmit' is camelCase"
refute_output --partial "event 'sessionStart'"
}
@test "hook: a flat Copilot-shaped file may use camelCase events" {
@test "hook: a flat Copilot-shaped file may use camelCase events when the package does not target Claude" {
printf 'targets:\n - copilot\n' >> "$PKG/apm.yml"
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"
}
@test "hook: camelCase in a flat file is a FAIL when the package targets Claude" {
printf 'targets: [claude, copilot]\n' >> "$PKG/apm.yml"
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"
}
@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"
}
@test "hook: targets are read from the nearest apm.yml walking up, 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"
run bash "$SCRIPT" "$PKG/sub/hooks/hooks.json"
assert_success
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"
}
@test "hook: a referenced script that does not exist is a FAIL" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/.apm/hooks/scripts/missing.sh"}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "script '.apm/hooks/scripts/missing.sh' does not exist"
}
@@ -127,7 +157,7 @@ teardown() {
chmod -x "$PKG/.apm/hooks/scripts/check.sh"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"./scripts/check.sh"}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "is run directly but is not executable"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ${PLUGIN_ROOT}/.apm/hooks/scripts/check.sh"}]}]}}'
@@ -139,7 +169,7 @@ teardown() {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/../outside.sh"}]}]}}'
touch "$TMPDIR/outside.sh"
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "resolves outside the package"
}
@@ -157,11 +187,26 @@ teardown() {
assert_output --partial "deprecated hook filename routing"
}
@test "hook: routing detection matches apm — case-insensitive, hooks-<target>, prefixed and combined stems" {
local name
for name in Claude-Hooks.json HOOKS-Copilot.json x-claude-hooks.json claude-codex-hooks.json; do
write_hook "$name" '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/$name"
assert_success
assert_output --partial "deprecated hook filename routing"
done
write_hook claude-hooks-extra.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/claude-hooks-extra.json"
assert_success
refute_output --partial "deprecated hook filename routing"
}
@test "hook: a symlinked hook file is a FAIL" {
write_hook real.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
ln -s real.json "$PKG/.apm/hooks/link.json"
run bash "$SCRIPT" "$PKG/.apm/hooks/link.json"
assert_failure
assert_failure 1
assert_output --partial "is a symlink"
}
@@ -176,7 +221,7 @@ teardown() {
@test "hook: an absolute script path is a FAIL; an absolute interpreter path is not" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"/usr/local/bin/check.sh","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "is an absolute path"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"/usr/bin/env true","timeout":5}]}]}}'
@@ -188,7 +233,7 @@ teardown() {
@test "hook: a bare relative path to a package script is a FAIL; a bare command is not" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":".apm/hooks/scripts/check.sh","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "is a bare relative path"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"npx some-tool --check","timeout":5}]}]}}'
@@ -200,10 +245,99 @@ teardown() {
@test "hook: a file contributing no entries is a FAIL" {
write_hook hooks.json '{"hooks":{}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure
assert_failure 1
assert_output --partial "contributes no hook entries"
}
@test "hook: a file whose every event list is empty is a FAIL" {
write_hook hooks.json '{"hooks":{"Stop":[],"PreToolUse":[]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "every event list is empty"
}
@test "hook: an entry with no handler is a FAIL, whether nested hooks is empty or absent" {
write_hook hooks.json '{"hooks":{"Stop":[{"matcher":"x"}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "event 'Stop' entry 0 has no handler"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "event 'Stop' entry 0 has no handler"
}
@test "hook: a non-command handler type (prompt) counts as a handler" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"prompt","prompt":"check","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_success
refute_output --partial "no handler"
}
@test "hook: a ./ script after an interpreter is existence-checked, as apm matches it" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ./scripts/missing.sh","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "script 'scripts/missing.sh' does not exist"
chmod -x "$PKG/.apm/hooks/scripts/check.sh"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ./scripts/check.sh","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_success
refute_output --partial "FAIL"
}
@test "hook: a ../ script path is a FAIL — apm reads it as ./ from the hook directory" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"bash ../scripts/x.sh","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "starts with ../"
}
@test "hook: the whole-token-quoted \${PLUGIN_ROOT} form passes clean" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"\"${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: a split-quoted \"\${PLUGIN_ROOT}\"/path is a FAIL — apm never rewrites it" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"\"${PLUGIN_ROOT}\"/.apm/hooks/scripts/check.sh","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "splits the quote"
}
@test "hook: a script path with a space is a FAIL, quoted or backslash-escaped" {
printf '#!/usr/bin/env bash\nexit 0\n' > "$PKG/.apm/hooks/scripts/my hook.sh"
chmod +x "$PKG/.apm/hooks/scripts/my hook.sh"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"\"${PLUGIN_ROOT}/.apm/hooks/scripts/my hook.sh\"","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "contains a space"
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/.apm/hooks/scripts/my\\ hook.sh","timeout":5}]}]}}'
run bash "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_failure 1
assert_output --partial "contains a space"
}
@test "hook: a hook file under a package-root hooks/ directory is audited, scripts resolved from the package root" {
mkdir -p "$PKG/hooks" "$PKG/scripts"
printf '#!/usr/bin/env bash\nexit 0\n' > "$PKG/scripts/run.sh"
chmod +x "$PKG/scripts/run.sh"
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/scripts/run.sh","timeout":5}]}]}}' > "$PKG/hooks/hooks.json"
run bash "$SCRIPT" "$PKG/hooks/hooks.json"
assert_success
refute_output --partial "FAIL"
printf '%s\n' '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"${PLUGIN_ROOT}/scripts/gone.sh","timeout":5}]}]}}' > "$PKG/hooks/hooks.json"
run bash "$SCRIPT" "$PKG/hooks/hooks.json"
assert_failure 1
assert_output --partial "script 'scripts/gone.sh' does not exist"
}
# ---------------------------------------------------------------------------
# Instructions
# ---------------------------------------------------------------------------
@@ -220,7 +354,7 @@ applyTo: "**/*.py"' 'Use type hints on public functions.'
@test "instruction: missing description is a FAIL" {
write_instruction python 'applyTo: "**/*.py"' 'Use type hints.'
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "'description' is missing or empty"
}
@@ -228,14 +362,14 @@ applyTo: "**/*.py"' 'Use type hints on public functions.'
write_instruction python 'description: x
applyTo: "**/*.py"' ''
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "body is empty"
}
@test "instruction: invalid frontmatter YAML is a FAIL" {
write_instruction python 'description: [unclosed' 'body'
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "frontmatter is not valid YAML"
}
@@ -261,7 +395,7 @@ name: python' 'body'
write_instruction empty 'description: x
applyTo: ""' 'body'
run bash "$SCRIPT" "$PKG/.apm/instructions/empty.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "applyTo is present but empty"
refute_output --partial "SUGGESTION no applyTo"
}
@@ -270,10 +404,21 @@ applyTo: ""' 'body'
write_instruction broken 'description: x
applyTo: "**/*.{py"' 'body'
run bash "$SCRIPT" "$PKG/.apm/instructions/broken.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "unbalanced braces or brackets"
}
@test "instruction: a non-string frontmatter key is a clean SUGGESTION, not a crash" {
write_instruction python 'description: x
applyTo: "**/*.py"
1: a
zeta: b' 'body'
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
assert_success
refute_output --partial "Traceback"
assert_output --partial "frontmatter key(s) 1, zeta"
}
@test "instruction: a top-level comma list with a brace group passes clean" {
write_instruction multi 'description: x
applyTo: "**/*.py, **/*.{pyi,pyx}"' 'body'
@@ -287,7 +432,7 @@ applyTo: "**/*.py, **/*.{pyi,pyx}"' 'body'
applyTo: "**/*.py"' 'body'
cp "$PKG/.apm/instructions/python.instructions.md" "$PKG/python.instructions.md"
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "also exists at the package root"
}
@@ -296,7 +441,7 @@ applyTo: "**/*.py"' 'body'
applyTo: "**/*.py"' 'body'
ln "$PKG/.apm/instructions/python.instructions.md" "$TMPDIR/python.instructions.md"
run bash "$SCRIPT" "$PKG/.apm/instructions/python.instructions.md"
assert_failure
assert_failure 1
assert_output --partial "is a hardlink"
}
@@ -317,7 +462,7 @@ input:
@test "prompt: missing description is a FAIL" {
write_prompt review-pr 'model: sonnet' 'Review the PR.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure
assert_failure 1
assert_output --partial "'description' is missing or empty"
}
@@ -327,23 +472,25 @@ input:
- name: pr_number
description: The PR' 'Review ${input:pr_number}.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure
assert_failure 1
assert_output --partial "yields arguments [name, description]"
}
@test "prompt: an invalid input name is a FAIL" {
write_prompt review-pr 'description: Review a PR.
input: [1pr]' 'Review.'
input:
- 1pr: "The PR"' 'Review.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure
assert_failure 1
assert_output --partial "input name '1pr' does not match"
}
@test "prompt: an undeclared \${input:x} and an unused input are both FAILs" {
write_prompt review-pr 'description: Review a PR.
input: [pr_number]' 'Review ${input:branch}.'
input:
- pr_number: "The PR"' 'Review ${input:branch}.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure
assert_failure 1
assert_output --partial "input: does not declare 'branch'"
assert_output --partial "input 'pr_number' is declared but the body never uses"
}
@@ -351,7 +498,7 @@ input: [pr_number]' 'Review ${input:branch}.'
@test "prompt: \${input:x} with no input: declared is a FAIL" {
write_prompt review-pr 'description: Review a PR.' 'Review ${input:pr_number}.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure
assert_failure 1
assert_output --partial "but no input: is declared"
}
@@ -369,10 +516,58 @@ allowedTools: Bash" 'Review the PR.'
assert_output --partial "'allowedTools' — use the kebab-case spelling"
}
@test "prompt: input: forms other than the object form are FAILs (primitive-author Must 3)" {
write_prompt review-pr 'description: Review a PR.
input: [pr_number]' 'Review ${input:pr_number}.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure 1
assert_output --partial "input entry 'pr_number' is a bare name"
write_prompt review-pr 'description: Review a PR.
input: pr_number' 'Review ${input:pr_number}.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure 1
assert_output --partial "input: is a bare name"
write_prompt review-pr 'description: Review a PR.
input:
pr_number: The PR' 'Review ${input:pr_number}.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure 1
assert_output --partial "input: is a map"
}
@test "prompt: a Not X -> Y boundary clause in the description is a SUGGESTION" {
write_prompt review-pr 'description: Review the PR with gitea-prs. Not fixing it -> skill-author.' 'Review the PR with gitea-prs.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_success
assert_output --partial "'Not X -> Y' boundary clause"
}
@test "prompt: a non-string frontmatter key is a clean SUGGESTION, not a crash" {
write_prompt review-pr 'description: Review a PR.
1: a
zeta: b' 'Review the PR.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_success
refute_output --partial "Traceback"
assert_output --partial "frontmatter key(s) 1, zeta"
}
@test "prompt: a *.prompt.md under an agents/ directory takes the prompt flow, not the agent flow" {
mkdir -p "$PKG/.apm/agents"
printf -- '---\ndescription: x\n---\n\nbody\n' > "$PKG/.apm/agents/x.prompt.md"
run bash "$SCRIPT" "$PKG/.apm/agents/x.prompt.md"
assert_failure 1
assert_output --partial "is not directly in a .apm/prompts/ directory"
refute_output --partial "counterpart"
}
@test "prompt: argument-hint alongside input: is a SUGGESTION" {
write_prompt review-pr 'description: Review a PR.
argument-hint: <pr>
input: [pr_number]' 'Review ${input:pr_number}.'
input:
- pr_number: "The PR"' 'Review ${input:pr_number}.'
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_success
assert_output --partial "argument-hint is set alongside input:"
@@ -392,6 +587,34 @@ $(printf 'line\n%.0s' $(seq 1 80))
write_prompt review-pr 'description: Review a pull request with gitea-prs.' 'Review the PR with gitea-prs.'
ln "$PKG/.apm/prompts/review-pr.prompt.md" "$TMPDIR/review-pr.prompt.md"
run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_failure
assert_failure 1
assert_output --partial "is a hardlink"
}
# ---------------------------------------------------------------------------
# Never ran (exit 2)
# ---------------------------------------------------------------------------
@test "exit 2: a PATH with no python3 names the missing dependency in primitive mode" {
write_hook hooks.json '{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"true"}]}]}}'
local emptybin="$TMPDIR/emptybin" cmd bash_bin
mkdir -p "$emptybin"
for cmd in cat sed; do
ln -s "$(command -v "$cmd")" "$emptybin/$cmd"
done
bash_bin="$(command -v bash)"
run env -i PATH="$emptybin" HOME="$HOME" "$bash_bin" "$SCRIPT" "$PKG/.apm/hooks/hooks.json"
assert_equal "$status" 2
assert_output --partial "python3 is required"
}
@test "exit 2: a python3 that cannot import PyYAML names the missing library in primitive mode" {
write_prompt review-pr 'description: Review a PR.' 'Review the PR.'
local noyaml="$TMPDIR/noyaml"
mkdir -p "$noyaml"
# Shadow PyYAML: PYTHONPATH precedes site-packages, so `import yaml` fails.
printf 'raise ImportError("PyYAML shadowed by the test")\n' > "$noyaml/yaml.py"
PYTHONPATH="$noyaml" run bash "$SCRIPT" "$PKG/.apm/prompts/review-pr.prompt.md"
assert_equal "$status" 2
assert_output --partial "PyYAML is required"
}