feat(kyberforge): refresh the apm install at SessionStart, not at push
Why --- ADR-0018 left deployed skills tracking the remote default branch with nothing watching for drift. The mechanism that was supposed to cover this, scripts/git-hooks/post-push, could never have worked: git has no client-side post-push hook. install.sh copied it into .git/hooks/ so it looked installed, and it had never once fired. Issue #78 reported it as skipping the gitea plugin; it was skipping everything. Refreshing on push was also the wrong shape. Your install goes stale when someone else merges, so a push of your own is neither necessary nor sufficient for staleness to have occurred. Implementation notes -------------------- kyberforge ships a SessionStart hook (startup matcher only) that runs `apm outdated`, and when anything is behind runs `apm update --yes` and returns reloadSkills:true so the running session picks up redeployed content. It exits silently with no apm.lock.yaml present, which keeps it inert for hosts that installed this plugin natively rather than through apm. Two findings drove the wiring, both verified rather than assumed: - apm resolves ${CLAUDE_PLUGIN_ROOT} against the installed package root, and `apm pack` keeps only *.json from .apm/hooks/. A .../hooks/<script> reference therefore points into the generated mirror where the script does not exist — apm reports "Hook script not found" and deploys a hook aimed at nothing. The reference must be .apm/-relative, and a test pins it. - apm's executable-trust gate is OFF unless apm.yml carries an `executables:` block; until now every hook, bin and MCP primitive a dependency shipped would have deployed unprompted. Root apm.yml now enables it. The allow key is version-pinned by apm's design, so a kyberforge version bump silently blocks the hook until the key is bumped too — called out in the block and the ADR. Also corrects ADR-0018 and AGENTS.md, which named `apm install` as the refresh command. It is not: `apm install` deploys from apm.lock.yaml's pinned commit and does not re-resolve refs. `apm update` does. Impact ------ Session startup costs ~0.7s when current and ~10.4s when six packages are behind. Auto-refresh rewrites apm.lock.yaml, so an unexplained modification to it after opening a session is expected; the emitted notice says so. .claude/settings.json stops being exactly {"hooks": {}} once the hook lands there — the merged entry is apm's own output, and the rule that nothing repo-authored goes in that file is unchanged. .claude/hooks/ and the .claude/apm-hooks.json sidecar are gitignored install output. The hook cannot install itself: dependencies resolve from the remote, so it takes effect only after this merges and `apm update` runs once against the new default branch. scripts/git-hooks/ is now empty. install.sh's copy block is kept and test-git-hooks-install.sh synthesizes its own fixture, so the mechanism stays tested without requiring a dead hook to exist. ADR: 0019 Refs: #78 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
This commit is contained in:
42
plugins/kyberforge/.apm/hooks/check-apm-current.sh
Executable file
42
plugins/kyberforge/.apm/hooks/check-apm-current.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# SessionStart: keep an apm-consumed install level with its remote.
|
||||
#
|
||||
# Packages declared as unpinned git refs resolve against the remote default
|
||||
# branch, so the deployed .claude/skills/ and .claude/agents/ go stale the
|
||||
# moment anyone merges. The staleness bites when a session loads skills, which
|
||||
# is why this runs at SessionStart rather than off a git hook — a pull is
|
||||
# neither necessary nor sufficient for the install to have drifted.
|
||||
#
|
||||
# Refreshes in place and asks the host to re-scan, so the running session picks
|
||||
# the new content up without a restart.
|
||||
#
|
||||
# Inert in any project that does not consume packages through apm.
|
||||
set -uo pipefail
|
||||
|
||||
# No lockfile means nothing was installed through apm here — e.g. a host that
|
||||
# installed this plugin natively. Say nothing and cost nothing.
|
||||
[[ -f apm.lock.yaml ]] || exit 0
|
||||
command -v apm > /dev/null 2>&1 || exit 0
|
||||
|
||||
# `apm outdated` exits 0 whether or not anything is stale, so the answer has to
|
||||
# come from its output. ~0.7s against six remote refs; a hung remote must not
|
||||
# hold the session open.
|
||||
outdated_output="$(timeout 60 apm outdated 2>&1)" || exit 0
|
||||
grep -q "outdated dependencies found" <<< "$outdated_output" || exit 0
|
||||
|
||||
stale_count="$(grep -oE '[0-9]+ outdated dependencies found' <<< "$outdated_output" | grep -oE '^[0-9]+' || true)"
|
||||
[[ "$stale_count" =~ ^[0-9]+$ ]] || stale_count="some"
|
||||
|
||||
# Only ever emit fixed text plus a digit-checked count — never interpolate
|
||||
# command output into the JSON, which would need escaping this cannot do safely.
|
||||
emit() {
|
||||
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","reloadSkills":%s,"additionalContext":"%s"}}\n' "$1" "$2"
|
||||
}
|
||||
|
||||
if timeout 300 apm update --yes > /dev/null 2>&1; then
|
||||
emit true "apm install was ${stale_count} package(s) behind the remote default branch and has been refreshed automatically; skills and agents were redeployed and re-scanned. apm.lock.yaml has been rewritten and is now a modified file in the working tree - commit it or discard it deliberately."
|
||||
else
|
||||
emit false "apm install is ${stale_count} package(s) behind the remote default branch and the automatic refresh failed. Deployed skills and agents may be stale. Run: apm update --yes"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,3 +1,16 @@
|
||||
{
|
||||
"hooks": {}
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh",
|
||||
"timeout": 320,
|
||||
"type": "command"
|
||||
}
|
||||
],
|
||||
"matcher": "startup"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user