Files
holocron/docs/spec/architecture.md
Defame1297 2e8732a8e5 build(apm): consume holocron plugins through apm instead of plugin install
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_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 17:15:17 +00:00

7.6 KiB

Architecture

Layered model

this repo (global defaults)
    └── scripts/install.sh → ~/.claude/         (Claude Code config + content)

project repo (local overrides)
    └── .claude/settings.json, CLAUDE.md (overrides global)

Content deployment model

scripts/install.sh is a deployer, not a composer. It sources scripts/deploy-manifest.sh and deploys three categories:

  • Files (DEPLOY_FILES): providers/claude-code/CLAUDE.md → ~/.claude/CLAUDE.md; providers/claude-code/settings.json → ~/.claude/settings.json; core/AGENTS.md → ~/.agents/AGENTS.md
  • Executables (DEPLOY_EXECUTABLES): providers/claude-code/statusline-command.sh → ~/.claude/statusline-command.sh (with +x)
  • Directories (DEPLOY_DIRS): core/ → ~/.claude/core/ (destination fully replaced on each deploy)

Skills are not deployed by install.sh. They are distributed as plugins and installed separately — in this repo by apm install against the dependencies.apm entries in the root apm.yml, which lands them in .claude/skills/ and .claude/agents/ (ADR-0018); elsewhere by claude plugin install <name>@holocron.

~/.claude/CLAUDE.md is a thin adapter, not a content source. It imports ~/.agents/AGENTS.md (always-on rules) and governance.md (always-on governance), then lists the content index. All always-on content lives in AGENTS.md files so other providers can import the same source without duplication.

Plugin model

Skills, agents, MCP servers, and hooks are distributed as self-contained plugin units under plugins/, installed independently — via apm install here, or claude plugin install <name>@holocron for a host consuming the marketplace natively (ADR-0018). Each plugin is an apm package: plugins/<name>/apm.yml plus a hand-authored plugins/<name>/.apm/{skills,agents,hooks,commands,instructions,extensions}/ tree (ADR-0015). There is no hand-maintained plugin.json — every manifest and every host-visible content directory is compiled from that source.

Two compilers produce the plugin roots you see in the tree:

  • apm pack compiles the manifests (ADR-0015). Per plugin: .claude-plugin/plugin.json and .github/plugin/plugin.json, both generated from plugins/<name>/apm.yml. Repo-wide, from the root apm.yml's marketplace: block: .claude-plugin/marketplace.json (apm's claude output profile) and .agents/plugins/marketplace.json (its codex profile, a differently-shaped file). Those two are the only marketplace outputs apm has profiles for — the third root manifest, .github/plugin/marketplace.json (Copilot CLI's legacy path), is a byte-identical mirror of the Claude one maintained by scripts/sync-marketplace-mirror.sh and gated by the check-marketplace-mirror-sync pre-push hook.
  • scripts/sync-plugin-content.sh compiles the content mirror (ADR-0017). It wraps apm pack --format plugin and copies the resulting bundle's flat agents/, skills/, commands/, instructions/, extensions/, and merged hooks/hooks.json back to the plugin root. Claude Code's installer convention-scans those flat paths and has no .apm/ awareness whatsoever, so the mirror exists solely to satisfy the host's discovery contract.

.apm/ is the sole hand-edited authoring source for plugin content. An edit made in the flat mirror is discarded by the next sync and is reported as drift by the check-plugin-content-sync pre-push hook. Hand-authored material that is not an .apm/ primitive — README.md, docs/, bin/, sources.md, .mcp.json, and per-plugin extras such as plugins/git/config.example.json, plugins/gitea/references/ and plugins/bin/evals/ — lives at the plugin root and is untouched by either compiler.

That immunity is positional, not by filename. Anything placed inside a mirrored directory is destroyed regardless of what it is: sync_dir runs rm -rf "$dst" before every copy, and sync_hooks_json does the same to hooks/. A hand-written README.md under plugins/<name>/hooks/ or plugins/<name>/skills/ is deleted by the next sync with no drift report, because a file with no .apm/ counterpart is simply absent from the regenerated tree. This has already cost the repo one document — plugins/kyberforge/hooks/README.md, since restored to plugins/kyberforge/docs/hooks.md. Plugin-root documentation belongs in docs/.

Governance layer

core/instructions/governance.md is the always-on governance instruction file. Unlike the on-demand instruction files in the content index, governance.md is loaded into every Claude session via @import in providers/claude-code/CLAUDE.md. This is a technical guarantee, not a behavioural instruction — @import causes Claude Code to expand and load the file at launch, before any interaction begins.

The governance layer has two phases:

  • Phase 1 (complete): instruction and documentation layer — governance.md loaded via @import; docs/ai-constitution.md and docs/wiki/HUMANS.md as human-facing reference; CONTEXT.md extended with governance domain language.
  • Phase 2 (planned): deterministic enforcement layer — pre-commit hooks, CI gates, secret scanning, licence scanning. Specified in docs/research/governance_principles/CONTROLS.md.

AGENTS.md pattern

This repo uses two AGENTS.md files as the provider-agnostic source of always-on rules (ADR-0003):

  • Repo-level AGENTS.md — instructions for agents working inside this repo (structure, key rules). Imported by repo CLAUDE.md via @AGENTS.md.
  • Global core/AGENTS.md — Communication and Behavior rules that apply across all projects. Deployed to ~/.agents/AGENTS.md; imported by ~/.claude/CLAUDE.md via @~/.agents/AGENTS.md.

Both CLAUDE.md files are thin adapters: they import from their respective AGENTS.md and add only Claude Code-specific syntax (@import, content index paths). They carry no original always-on content.

This repo also has a CLAUDE.md at its root — the Claude Code entry point for working in this repo. It imports AGENTS.md and nothing else; there is no @CONTEXT.md import. It is not import-only either: below the import sits a fenced <!-- rtk-instructions v2 --> … <!-- /rtk-instructions --> block carrying the RTK command-prefix convention, which is tool-specific content with no AGENTS.md source. This is distinct from providers/claude-code/CLAUDE.md, which is the global config deployed to ~/.claude/.

CONTEXT.md is therefore not always-loaded. AGENTS.md instructs agents to read it at session start, which is a behavioural instruction, not an @import guarantee — LESSONS.md's 2026-05-17 entry proposed adding the import and it was never applied. Treat that entry as open work rather than a record of a landed change.

Provider model

core/ is never tool-specific. providers/ is never shared. When adding a new provider, write an adapter in providers/<name>/ that translates core content into the tool's expected format and location. The core content itself does not change.

Architectural decisions

Key hard-to-reverse decisions are recorded as ADRs in docs/adr/. There is no index file — the directory holds 17 numbered ADRs whose filenames state their decision, so ls docs/adr/ is the index. Read a superseding ADR before the one it supersedes: ADR-0015 (apm as the authoring source of truth) supersedes ADR-0001 and moots ADR-0006, and ADR-0017 corrects ADR-0015's host-discovery gap. Entry points for the structure described on this page: ADR-0002 (two-tier CLAUDE.md), ADR-0003 (AGENTS.md as the provider-agnostic entry point), ADR-0015 and ADR-0017 (the two compilers behind the plugin roots).