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:
@@ -34,16 +34,58 @@ mirrored file; the `check-plugin-content-sync` pre-push hook reports it as drift
|
||||
}
|
||||
```
|
||||
|
||||
Events (**partial list**): `PreToolUse`, `PostToolUse`, `Notification`, `Stop`. Claude Code's plugin
|
||||
hook set is larger — `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreCompact` and
|
||||
`SubagentStop` also exist — and this repo's vendored corpus does not enumerate it anywhere:
|
||||
`docs/research/docs/claude-code-plugins/configuration.md:100` describes the file as "Event handlers
|
||||
(PreToolUse, PostToolUse, etc.)", and `agent-definition.md:53` covers only the per-agent `hooks`
|
||||
field, not the plugin-level set. Treat the four names above as the ones this repo has verified, not
|
||||
as the schema. Check Claude Code's own hooks documentation before wiring an event not listed here.
|
||||
Events (**partial list**): `PreToolUse`, `PostToolUse`, `Notification`, `Stop`, and `SessionStart`
|
||||
(verified end-to-end by the hook below). Claude Code's plugin hook set is larger — `SessionEnd`,
|
||||
`UserPromptSubmit`, `PreCompact` and `SubagentStop` also exist — and this repo's vendored corpus does
|
||||
not enumerate it anywhere: `docs/research/docs/claude-code-plugins/configuration.md:100` describes
|
||||
the file as "Event handlers (PreToolUse, PostToolUse, etc.)", and `agent-definition.md:53` covers
|
||||
only the per-agent `hooks` field, not the plugin-level set. Treat the five names above as the ones
|
||||
this repo has verified, not as the schema. Check Claude Code's own hooks documentation before wiring
|
||||
an event not listed here.
|
||||
|
||||
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin — the plugin runs from a cache
|
||||
path after install, not its original repo location.
|
||||
## Referencing a script — use the `.apm/` path, not the mirror
|
||||
|
||||
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin; the plugin runs from a cache or
|
||||
`apm_modules/` path after install, not its original repo location. **Address the script at its
|
||||
`.apm/` path:**
|
||||
|
||||
```json
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh"
|
||||
```
|
||||
|
||||
The obvious-looking `${CLAUDE_PLUGIN_ROOT}/hooks/check-apm-current.sh` does not work, and fails
|
||||
quietly enough to be worth spelling out. apm resolves the placeholder against the installed package
|
||||
root, where `hooks/` is the **generated mirror** — and `apm pack` merges only `*.json` out of
|
||||
`.apm/hooks/`, dropping every non-JSON file. So the mirror contains `hooks.json` and nothing else.
|
||||
apm prints `Hook script not found: .../hooks/check-apm-current.sh` and then deploys the hook anyway,
|
||||
pointing at a path with no file behind it.
|
||||
|
||||
Nor can the script be hand-placed in `plugins/kyberforge/hooks/` to satisfy that path:
|
||||
`sync-plugin-content.sh` runs `rm -rf` on the directory before every rebuild (ADR-0017), so it would
|
||||
be deleted on the next sync with no drift warning — the same trap that ate this document's
|
||||
predecessor.
|
||||
|
||||
`tests/test-apm-current-hook.sh` pins the reference so a well-meaning "simplification" back to
|
||||
`hooks/` fails the suite rather than silently disabling the hook.
|
||||
|
||||
## Deployed shape
|
||||
|
||||
At install, apm merges the event bindings into `.claude/settings.json`, copies the referenced script
|
||||
to `.claude/hooks/<pkg>/` (preserving its executable bit, preserving the `.apm/hooks/` subpath), and
|
||||
rewrites `command` to a `${CLAUDE_PROJECT_DIR}`-relative path. Ownership of its own entries is
|
||||
tracked in a `.claude/apm-hooks.json` sidecar, so an uninstall removes them without touching
|
||||
hand-authored hooks. Both `.claude/hooks/` and the sidecar are gitignored install output.
|
||||
|
||||
Note that apm's **executable-trust gate is off** unless the consuming project's `apm.yml` has an
|
||||
`executables:` block — without one, package hooks deploy with no prompt. See ADR-0019.
|
||||
|
||||
## The SessionStart hook
|
||||
|
||||
`check-apm-current.sh` keeps an apm-consumed install level with its remote: it runs `apm outdated`,
|
||||
and if anything is behind, runs `apm update --yes` and returns `reloadSkills: true` so the running
|
||||
session picks up the redeployed content. It exits silently when there is no `apm.lock.yaml` in the
|
||||
working directory, which makes it inert for any host that installed this plugin natively rather than
|
||||
through apm. Rationale, measurements, and the failure modes are in ADR-0019.
|
||||
|
||||
## GitHub Copilot CLI
|
||||
|
||||
|
||||
Reference in New Issue
Block a user