AGENTS.md and CONTEXT.md asserted that the `<plugin>:<skill>` form "no longer resolves here". It does: ~/.claude.json still enables core, git, gitea, kyberforge and lint at user scope, which ADR-0018 left in place deliberately. Both names are live at once, so a working `gitea:gitea-prs` is the user-scope copy answering — not evidence that the apm install is broken and not something to "fix". ADR-0018 contradicted itself on this, claiming every namespaced reference went stale while its own "User scope is untouched" consequence said otherwise; recorded as a dated correction alongside the existing one. Bare names stay the documented default. Five stale pre-push hook counts updated for the new check-executables-allow-sync gate: 13 -> 14 repo-defined hooks, 15 -> 16 reported by pre-commit, eleven -> twelve passing offline. The gate reads two local manifests and makes no network call, so the SKIP pair for offline pushes stays exactly two. "Four pre-push hooks shell out to apm" is unchanged and still correct — the new hook parses YAML directly. ADR-0019 gains the timeout arithmetic, the singular/plural failure and why mocking every apm call hid it, and a consequence recording that the trust gate is keyed on version rather than content: an edit to a hook script landing on main deploys and executes unattended on the next session start, since the dependency is unpinned and the hook runs `apm update --yes`. That is accepted, not overlooked, but it is why the gate should not be read as a supply-chain control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
133 lines
8.6 KiB
Markdown
133 lines
8.6 KiB
Markdown
# This repo installs its own plugins through apm, not Claude Code's native plugin install
|
|
|
|
ADR-0015 moved plugin **authoring** to apm; ADR-0017 added the flat content mirror that keeps the
|
|
authored `.apm/` tree discoverable by hosts that install natively. Both are about producing the
|
|
marketplace. This ADR is about consuming it: how the plugins get onto the machine this repo is
|
|
worked on.
|
|
|
|
**Status: executed (2026-08-14).** All six packages are installed into `/root/ai-development` by
|
|
`apm install`; the six native project-scope installs (`claude plugin uninstall <name>@holocron
|
|
--scope project`) are gone and `.claude/settings.json`'s `enabledPlugins` block is empty.
|
|
|
|
## Context
|
|
|
|
Until now the repo consumed its own output the same way any user would: `claude plugin install
|
|
<name>@holocron`, six plugins enabled per-project in `.claude/settings.json`, the `holocron`
|
|
marketplace registered in `~/.claude/plugins/known_marketplaces.json` with `autoUpdate: true`.
|
|
That worked. It also meant the repo's dogfooding stopped one layer short of the tooling it
|
|
publishes: `kyberforge` ships `apm-workflow` and `apm-install` skills describing an install path
|
|
the repo itself did not take.
|
|
|
|
apm supports both scopes. `apm install --global` deploys to `~/.claude/`; plain `apm install`
|
|
deploys to the project. Global was rejected deliberately — the switch should be provable in one
|
|
repo before it changes how every other project on the machine resolves its skills.
|
|
|
|
## Decision
|
|
|
|
Root `apm.yml` declares all six packages under `dependencies.apm`, each as a `git:`/`path:` object
|
|
against the holocron remote:
|
|
|
|
```yaml
|
|
dependencies:
|
|
apm:
|
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
|
path: plugins/core
|
|
```
|
|
|
|
`apm install` deploys them to `.claude/skills/<name>/` and `.claude/agents/<name>.md`.
|
|
|
|
Three sub-decisions inside that:
|
|
|
|
- **Object form over the `<name>@holocron` marketplace alias.** The alias is shorter and apm
|
|
resolves it correctly (verified end-to-end against this remote), but it first requires
|
|
`apm marketplace add`, which writes to `~/.apm/marketplaces.json` — user scope, outside the
|
|
repo, and absent on a fresh clone. The object form needs nothing beyond the committed manifest.
|
|
- **Remote source over local path.** apm accepts `path: /root/ai-development/plugins/<name>` as a
|
|
local dependency, which would make the working tree live instantly. Rejected: it erases the
|
|
distinction between editing a skill and shipping one, which is the entire point of having a
|
|
marketplace. The remote form keeps the repo running the same released content every other
|
|
consumer gets.
|
|
- **Unpinned against the default branch.** Parity with the `autoUpdate: true` the native install
|
|
had. apm warns on every install (`6 dependencies unpinned`); accepted knowingly. Pinning is a
|
|
per-entry `ref:` away once the repo tags releases per package — today `git tag` lists one tag
|
|
total, so there is nothing meaningful to pin to.
|
|
|
|
## Consequences
|
|
|
|
**Skills gain an unnamespaced name.** apm deploys plain project skills, so `git:git-commits` also
|
|
answers to `git-commits` and `kyberforge:skill-audit` to `skill-audit`. This is not configurable —
|
|
a project skill has no plugin to prefix. `AGENTS.md` and `CONTEXT.md` are updated to name the bare
|
|
form, which is what apm deploys and the only form a repo consuming holocron through apm gets.
|
|
|
|
**Correction (2026-08-14): the namespaced form did not stop resolving.** An earlier revision of
|
|
this consequence said every `<plugin>:<skill>` reference "was stale the moment the switch landed",
|
|
and `AGENTS.md`/`CONTEXT.md` were written to match. That contradicts the "User scope is untouched,
|
|
deliberately" consequence below, and the contradiction resolves against it: `~/.claude.json` still
|
|
enables `core`, `git`, `gitea`, `kyberforge` and `lint` at user scope, so both names are live at
|
|
once and a working `gitea:gitea-prs` is the user-scope copy answering. That doubling is the same
|
|
"present twice under two names" outcome the "Keeping both install paths" alternative was rejected
|
|
for — reached by leaving user scope alone rather than by adopting it, which is why it is a
|
|
consequence to record rather than a decision to revisit. Prefer the bare name regardless: it
|
|
survives those user-scope installs eventually being converted, and the namespaced form still
|
|
resolves for anyone installing holocron natively, so skill bodies written for both audiences
|
|
should name the bare skill.
|
|
|
|
**apm owns `.claude/settings.json`.** (ADR-0019 supersedes the "exactly `{"hooks": {}}`" claim
|
|
below — once a package ships a hook, apm merges it into that file and the merged entry is apm's own
|
|
output. The rule that nothing repo-authored goes in the file is unchanged.) `apm audit --ci` replays the install into a scratch tree and
|
|
diffs it against the worktree. apm's hook integrator writes that file, so the replay expects
|
|
exactly what apm would have written — `{"hooks": {}}` — and any repo-owned key in it is permanent
|
|
drift that fails the `apm-audit-ci` pre-push hook. Verified both directions: with the pre-existing
|
|
`enabledPlugins` block present, `1 of 10 check(s) failed`; reduced to `{"hooks": {}}`,
|
|
`All 10 check(s) passed`. Nothing was lost in that reduction — `enabledPlugins` was empty after the
|
|
native uninstall and the only `hooks` entry was an empty `PreToolUse: []` — but it does mean the
|
|
file is no longer available for repo-owned settings. Machine-specific settings go in the gitignored
|
|
`.claude/settings.local.json`, which apm does not deploy; shared enforcement belongs in
|
|
`.pre-commit-config.yaml`, where this repo already keeps it.
|
|
|
|
**`apm_modules/` breaks naive tree walks.** apm materializes a full copy of every dependency there
|
|
— including this repo's own plugins, `.bats` files and all. The dependency copies resolve their
|
|
bats helpers relative to their own root, not this repo's, so `tests/run-tests.sh` went from 167
|
|
tests passing to `334 tests, 167 failures` on the first install. Both discovery walks
|
|
(`tests/run-bats.sh`, `tests/run-tests.sh`) now exclude `apm_modules/`, on the find side and on the
|
|
`git ls-files` side that derives the expected set. Any future script that walks the repo tree needs
|
|
the same exclusion.
|
|
|
|
**Install output is gitignored; the lockfile is not.** `.claude/skills/`, `.claude/agents/`, and
|
|
`apm_modules/` are regenerated by `apm install`. Committing the deployed skills would add a third
|
|
mirror of content ADR-0017 already governs two copies of. `apm.lock.yaml` is committed — it is what
|
|
makes the install reproducible, and `apm audit --ci` checks it.
|
|
|
|
**MCP survived the switch; hooks were never at risk.** apm read `plugins/bin/.mcp.json` as a
|
|
self-defined direct-dependency MCP server and configured `obsidian` into the repo's `.mcp.json`
|
|
unprompted. The `gitea` and `context7` servers were never plugin-provided — they live in
|
|
`~/.claude.json` and are untouched. Every plugin's `.apm/hooks/hooks.json` is `{"hooks": {}}`, so
|
|
apm's "contributed no entries to claude settings; skipped" warning on `kyberforge` and `lint` is
|
|
accurate and harmless.
|
|
|
|
**A `.apm/` edit now needs a round trip.** The dependency resolves from the remote, so an edit is
|
|
invisible to the running session until it is pushed and the install is refreshed. Under the native
|
|
install with `autoUpdate` the shape was the same; it was more noticeable here at first because the
|
|
refresh is a manual step where marketplace auto-update was not — ADR-0019 automates it at
|
|
`SessionStart`.
|
|
|
|
**Correction (2026-08-14): the refresh command is `apm update`, not `apm install`.** An earlier
|
|
revision of this paragraph named `apm install`, which is wrong: `apm install` deploys from the
|
|
pinned `resolved_commit` in `apm.lock.yaml` and does not re-resolve refs (`apm install --force`
|
|
documents this explicitly — "does NOT refresh refs; use 'apm update' for that"). Running it after a
|
|
merge redeploys the same content and reports success.
|
|
|
|
**User scope is untouched, deliberately.** `bin@holocron`, `gitea@holocron`, and a stale
|
|
`hello-world@holocron` remain natively installed at user scope, and every project other than this
|
|
one still resolves its skills that way. Converting them is a separate decision with a blast radius
|
|
beyond this repo.
|
|
|
|
## Alternatives considered
|
|
|
|
- **`apm install --global`.** Verified working in an isolated `HOME`: user-scope deploys land in
|
|
`~/.claude/skills/` and `~/.claude/agents/`, and it is the only scope where a plugin's `bin/`
|
|
executables deploy (moot here — every `bin/` in this repo is empty but for a README). Deferred,
|
|
not rejected: it changes skill resolution for every project on the machine at once.
|
|
- **Keeping both install paths.** Rejected: the same skill would be present twice under two names,
|
|
and `.claude/settings.json` cannot hold `enabledPlugins` without failing `apm audit --ci`.
|