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

@@ -45,11 +45,12 @@ EOF
chmod +x "$FAKE_BIN/apm"
}
# CLAUDE_PROJECT_DIR is cleared rather than merely left alone: a session in this
# repo exports it, and an inherited value would point every case at the real
# repo root (which has a real apm.lock.yaml) instead of the fixture. The
# project-directory cases below set it deliberately.
run_hook() { (cd "$WORK" && env -u CLAUDE_PROJECT_DIR PATH="$FAKE_BIN:$PATH" bash "$HOOK" 2>/dev/null); }
# CLAUDE_PROJECT_DIR is set to the fixture rather than inherited: a session in
# this repo exports it, and an inherited value would point every case at the
# real repo root (which has a real apm.lock.yaml) instead of the fixture. It
# must be set, not cleared — the hook is Claude Code only and exits at once
# without it. The project-directory cases below vary it deliberately.
run_hook() { (cd "$WORK" && env CLAUDE_PROJECT_DIR="$WORK" PATH="$FAKE_BIN:$PATH" bash "$HOOK" 2>/dev/null); }
# Same, with an explicit cwd and CLAUDE_PROJECT_DIR. $1 is the cwd; $2 the value
# for CLAUDE_PROJECT_DIR, or the literal `-` to leave it unset.
@@ -86,7 +87,7 @@ echo "--- inert when apm is absent ---"
# ---------------------------------------------------------------------------
rm -f "$WORK/update-was-called"
out="$( (cd "$WORK" && PATH="$(dirname "$(command -v bash)")" bash "$HOOK" 2>/dev/null) )"; rc=$?
out="$( (cd "$WORK" && env CLAUDE_PROJECT_DIR="$WORK" PATH="$(dirname "$(command -v bash)")" bash "$HOOK" 2>/dev/null) )"; rc=$?
[[ $rc -eq 0 ]] && pass "exits 0 when apm is not on PATH" || fail "should exit 0 when apm is missing"
[[ -z "$out" ]] && pass "emits nothing when apm is not on PATH" || fail "should stay silent when apm is missing"
@@ -252,7 +253,7 @@ echo "--- anchors on the project root, not the session cwd ---"
# subdirectory of an apm-consuming repo therefore no-opped silently — and would
# have run `apm outdated`/`apm update` against that wrong directory had the
# guard passed. Claude Code exports CLAUDE_PROJECT_DIR for SessionStart hooks,
# so that is the anchor; the cwd is only the fallback.
# so that is the anchor, with no cwd fallback.
ELSEWHERE="$WORK/elsewhere"
mkdir -p "$ELSEWHERE"
rm -f "$ELSEWHERE/apm.lock.yaml"
@@ -269,17 +270,30 @@ out="$(run_hook_in "$ELSEWHERE" "$WORK")"
grep -q "6 package" <<< "$(json_field additionalContext <<< "$out")" \
&& pass "reports the count found via CLAUDE_PROJECT_DIR" || fail "should report the count"
# The fallback is not cosmetic: a host that installed this plugin natively sets
# no CLAUDE_PROJECT_DIR, and the hook must stay inert-but-harmless there rather
# than erroring on an unset variable (the script runs under `set -u`).
# Claude Code only (ADR-0019, amendment 2026-09-28). apm deploys this hook to
# Copilot and Codex too, and in a consumer the lockfile guard passes there —
# apm wrote the lock. A host that sets no CLAUDE_PROJECT_DIR must therefore
# exit before any apm call, even with a lockfile in the cwd and a stale install
# on offer; the old cwd fallback ran `apm update --yes` under such a host.
rm -f "$WORK/update-was-called" "$WORK/apm-cwd"
out="$(run_hook_in "$WORK" "-")"
[[ -f "$WORK/update-was-called" ]] \
&& pass "falls back to the cwd when CLAUDE_PROJECT_DIR is unset" \
|| fail "must still work with no CLAUDE_PROJECT_DIR in the environment"
[[ "$(cat "$WORK/apm-cwd" 2>/dev/null)" == "$WORK" ]] \
&& pass "runs apm in the cwd under the fallback" \
|| fail "apm ran in '$(cat "$WORK/apm-cwd" 2>/dev/null)' — should be the cwd"
out="$(run_hook_in "$WORK" "-")"; rc=$?
[[ $rc -eq 0 ]] && pass "exits 0 when CLAUDE_PROJECT_DIR is unset" \
|| fail "exited $rc with no CLAUDE_PROJECT_DIR — must exit 0"
[[ -z "$out" ]] && pass "stays silent when CLAUDE_PROJECT_DIR is unset" \
|| fail "emitted output with no CLAUDE_PROJECT_DIR — a non-Claude host must see nothing"
[[ ! -f "$WORK/apm-cwd" ]] \
&& pass "runs no apm command when CLAUDE_PROJECT_DIR is unset" \
|| fail "ran apm with no CLAUDE_PROJECT_DIR — a non-Claude host must never reach apm"
[[ ! -f "$WORK/update-was-called" ]] \
&& pass "does not run apm update when CLAUDE_PROJECT_DIR is unset" \
|| fail "ran apm update under a host that is not Claude Code"
# Set but empty is the same as unset: there is no project root to anchor on.
rm -f "$WORK/update-was-called" "$WORK/apm-cwd"
out="$(run_hook_in "$WORK" "")"; rc=$?
[[ $rc -eq 0 && -z "$out" && ! -f "$WORK/apm-cwd" ]] \
&& pass "treats an empty CLAUDE_PROJECT_DIR as unset" \
|| fail "an empty CLAUDE_PROJECT_DIR must exit 0 silently without running apm"
rm -f "$WORK/update-was-called" "$WORK/apm-cwd"
out="$(run_hook_in "$ELSEWHERE" "$ELSEWHERE")"; rc=$?
@@ -296,9 +310,9 @@ echo ""
echo "--- hooks.json wiring ---"
# ---------------------------------------------------------------------------
# apm resolves script paths relative to the package root, and `apm pack` keeps
# only *.json from .apm/hooks/ — so a ${PLUGIN_ROOT}/hooks/... reference
# points at a directory the script never reaches. It must be .apm/-relative, and
# apm resolves script paths relative to the package root, and the script lives
# under .apm/hooks/ — so a ${PLUGIN_ROOT}/hooks/... reference points at a
# directory that does not exist. It must be .apm/-relative, and
# it uses apm's target-neutral token, which apm rewrites identically to
# ${CLAUDE_PLUGIN_ROOT} for every target (ADR-0019, amendment 2026-09-28).
referenced="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d["hooks"]["SessionStart"][0]["hooks"][0]["command"])' "$HOOKS_JSON")"