feat(apm): consume plugins through apm and keep the install fresh at SessionStart #98
Reference in New Issue
Block a user
Delete Branch "feat/apm-consumed-install"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Switches this repo from
claude plugin install <name>@holocronto consuming its own published packages through apm, then fixes the fact that nothing was keeping that install current.Closes #78.
1. apm-consumed install (ADR-0018)
Root
apm.ymldeclares all six packages underdependencies.apmasgit:/path:objects against the holocron remote.apm installdeploys them to.claude/skills/and.claude/agents/.<name>@holocronalias — an alias needsapm marketplace add, which writes to~/.apm/marketplaces.json(user scope, absent on a fresh clone). The object form needs nothing beyond the committed manifest.autoUpdate: truethe native install had.Consequences worth knowing before review:
git:git-commitsis nowgit-commits; a project skill has no plugin to prefix.AGENTS.mdandCONTEXT.mdare updated. The namespaced form still resolves for anyone installing holocron natively..claude/settings.json.apm audit --cireplays the install into a scratch tree and diffs, so any repo-authored key there is permanent drift. Verified both ways: with the oldenabledPluginsblock,1 of 10 check(s) failed; reduced to apm's own output,All 10 check(s) passed.apm_modules/breaks naive tree walks. apm materializes a full copy of every dependency there,.batsfiles included, which took the suite from 167 passing to334 tests, 167 failureson first install. Both discovery walks now exclude it.2. SessionStart freshness hook (ADR-0019)
scripts/git-hooks/post-pushwas supposed to cover staleness. It could never have worked: git has no client-sidepost-pushhook.install.shcopied it into.git/hooks/, so it looked installed and had never once fired. #78 reported it as skipping thegiteaplugin — it was skipping everything. Both apparent tests only checked that the script behaved when invoked directly and that install.sh copied the file; neither asserted git ever runs it.Refreshing on push is also the wrong shape: your install goes stale when someone else merges, so a push of your own is neither necessary nor sufficient.
kyberforge now ships a
SessionStarthook (startupmatcher only) that runsapm outdated, and when anything is behind runsapm update --yesand returnsreloadSkills: trueso the running session picks up redeployed content. It exits silently with noapm.lock.yamlpresent, which keeps it inert for hosts that installed the plugin natively.Two findings drove the wiring, both verified in a sandbox rather than assumed:
${CLAUDE_PLUGIN_ROOT}against the installed package root, andapm packkeeps only*.jsonfrom.apm/hooks/. So a.../hooks/<script>reference points into the generated mirror where the script does not exist — apm printsHook script not foundand deploys a hook aimed at nothing. The reference must be.apm/-relative, and a test pins it. The script also cannot be hand-placed inplugins/kyberforge/hooks/, which every content syncrm -rfs.apm approve --listreportsExecutable-trust gate disabled -- all executables deployuntil anexecutables:block exists. Every hook, bin, and MCP primitive a dependency shipped would have deployed unprompted. Rootapm.ymlnow enables it.3. Release
kyberforge
1.4.1 → 1.5.0(MINOR); marketplace and root manifest0.3.4 → 0.4.0, per the convention inbb9158d. Theexecutables.allowkey moves tokyberforge#1.5.0in the same commit — it is version-pinned by apm's design, so shipping the hook without the bump would have silently blocked it.Also corrected
ADR-0018 and
AGENTS.mdboth namedapm installas the refresh command. It is not:apm installdeploys fromapm.lock.yaml's pinned commit and does not re-resolve refs (apm install --forcesays so explicitly — "does NOT refresh refs; use 'apm update' for that"). Running it after a merge redeploys the same content and reports success.Verification
bash tests/run-tests.sh— 18 suites, 0 skipped, 0 failedpre-commit run --hook-stage pre-push --all-files— all 15 pass, including both network hooks andapm pack --check-cleantests/test-apm-current-hook.sh— 23 assertions withapmmocked.claude/settings.json, copied the script to.claude/hooks/kyberforge/, tracked ownership in anapm-hooks.jsonsidecar, and the auto-update path refreshed a deliberately stale lock and redeployed 39 skillsMeasured cost at session start: ~0.7 s when current, ~10.4 s when six packages are behind and the refresh runs.
Reviewer notes
apm updateruns once against the newmain..claude/settings.jsonwill stop being{"hooks": {}}once the hook lands there. The merged entry is apm's own output and belongs in a follow-up commit; the rule that nothing repo-authored goes in that file is unchanged.apm.lock.yaml. An unexplained modification to it after opening a session is expected, not a bug. This was a deliberate choice over report-only; the emitted notice says so.🤖 Generated with Claude Code
https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
Why: The repo published apm packages but consumed them the old way — `claude plugin install <name>@holocron`, six plugins enabled per project. Dogfooding stopped one layer short of the install tooling kyberforge itself ships. Implementation notes: - Root apm.yml declares the six packages as dependencies.apm git+path objects against the holocron remote. Object form over `<name>@holocron` aliases on purpose: an alias first needs `apm marketplace add`, which writes to ~/.apm/marketplaces.json — user scope, absent on a fresh clone. Unpinned against the default branch, matching the autoUpdate the native install had. - apm.lock.yaml is committed; .claude/skills/, .claude/agents/ and apm_modules/ are gitignored regenerable install output. Committing the deployed skills would add a third mirror of content ADR-0017 already governs two copies of. - .mcp.json is generated by apm from plugins/bin/.mcp.json, so the obsidian MCP server survives the switch. - .claude/settings.json is reduced to {"hooks": {}}. apm replays the install into a scratch tree and diffs, so any repo-owned key there is permanent drift that fails apm-audit-ci. Nothing was lost: enabledPlugins was empty after the uninstall and the only hooks entry was PreToolUse: []. - tests/run-bats.sh and tests/run-tests.sh exclude apm_modules/. It holds a full copy of every plugin, and a copied .bats file resolves its helpers against the dependency root rather than this repo — 334 tests, 167 failures before the exclusion. Impact: Skills are now unnamespaced — `git-commits`, not `git:git-commits` — because apm deploys plain project skills with no plugin to prefix. AGENTS.md, CONTEXT.md and docs/spec/architecture.md are updated accordingly. Root apm.yml now declares dependencies, which arms apm-audit-ci's lockfile-exists check for the root manifest. External consumers are unaffected: the marketplace manifests are untouched and `apm pack --check-clean` stays clean. Project scope only. ADR: 0018 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DTReviewed and fixed in
099cf58..c9fe2e8.Five findings, all resolved on this branch:
executables.allowself-disabled on the next kyberforge release. apm matches the key by exactname#versiondict lookup — no wildcard, no version-less form — so a version bump without a key edit silently stops the hook deploying. Now gated at pre-push bycheck-executables-allow-sync.hooks.jsondeclared 320s against a 60+300s worst case, so a slow remote let the host kill the hook mid-update. Now 380, with a test asserting the invariant rather than the literal.<plugin>:form does still resolve here. Five plugins remain enabled at user scope, so both names are live; AGENTS.md/CONTEXT.md said otherwise and ADR-0018 contradicted itself. Corrected.1 outdated dependency foundin the singular; the hook matched only the plural. With six independently-merging dependencies, one-behind is the ordinary case — the freshness mechanism failed most often in exactly the situation it exists for. Found only by adding a probe that runs the realapm outdatedinstead of a mock; every prior assertion mocked apm, which is the same blind spot that letpost-pushlook tested for six weeks.CLAUDE_PROJECT_DIR.Also pinned
@bitbonsai/mcpvault@latest→@0.15.0(bin 1.1.3, marketplace 0.4.1) — unpinned third-party npm executing at every session start, now repo-root committed content.Suite 18 → 19 green, all 16 pre-push hooks pass. Note the PR body above is now stale on four numbers:
timeout: 320, 23 assertions, 18 suites, 13 pre-push hooks.