Files
holocron/AGENTS.md
Defame1297 dee56c506a 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
2026-08-14 17:46:38 +00:00

16 KiB

Working in this repo

This repo is the global AI development configuration repository — the authoritative source for agent definitions, skills, workflows, and prompts across all projects. Built as a homelab tool intended to scale to professional environments.

Structure

  • plugins/ — installable plugin units; each is an apm package (apm.yml + .apm/) carrying skills, agents, hooks, MCP servers, and bundled assets. This repo consumes them through apm, not Claude Code's native plugin install: root apm.yml declares all six as dependencies.apm git+path entries against the holocron remote, and apm install deploys them into .claude/skills/ and .claude/agents/ (both gitignored). External consumers can still install natively via claude plugin install <name>@holocron — the marketplace manifests are unchanged
  • providers/claude-code/ — Claude Code adapter (deployed to ~/.claude/ via install.sh)

Edit .apm/, never the flat mirror

Inside a plugin, plugins/<name>/.apm/ is the only hand-edited source for plugin content — the skills, agents, commands, instructions, extensions and hooks a host discovers. Everything in a plugin root that mirrors an .apm/ primitive, plus both plugin.json manifests, is generated:

  • scripts/sync-plugin-content.sh generates the flat plugins/<name>/{skills,agents,commands,instructions,extensions}/ directories and the merged plugins/<name>/hooks/hooks.json (ADR-0017)
  • apm pack generates both per-plugin manifests — plugins/<name>/.claude-plugin/plugin.json and plugins/<name>/.github/plugin/plugin.json — and two of the three root marketplace manifests: .claude-plugin/marketplace.json (apm's claude output profile) and .agents/plugins/marketplace.json (its codex profile, a differently-shaped file) (ADR-0015)
  • scripts/sync-marketplace-mirror.sh generates the third, .github/plugin/marketplace.json — Copilot CLI's legacy manifest path. No apm output profile targets it: apm ships exactly two marketplace output profiles, claude and codex (documented in plugins/kyberforge/.apm/skills/apm-workflow/references/marketplace.md). The mirror is a byte-identical copy of .claude-plugin/marketplace.json, gated by the check-marketplace-mirror-sync pre-push hook. Do not expect apm pack to refresh it — that assumption is exactly the drift this pair exists to prevent

A plugin root is not wholly generated. Material that is not an .apm/ primitive is hand-authored there and no compiler touches it: README.md, docs/, bin/, sources.md, .mcp.json, plus per-plugin extras like plugins/git/config.example.json, plugins/gitea/references/ and plugins/bin/evals/. Edit those in place — they have no .apm/ source, and looking for one wastes a search. The rule is per-path, not per-directory: plugins/<name>/skills/ is generated, plugins/<name>/docs/ is not. docs/spec/architecture.md carries the same carve-out.

One qualification: "hand-authored, untouched" holds only at the plugin root. A file placed inside a mirrored directory is destroyed — sync_dir runs rm -rf "$dst" before every copy, so a README.md under plugins/<name>/hooks/ or plugins/<name>/skills/ is deleted on the next sync whether or not .apm/ has a counterpart. Put root-level plugin documentation in docs/, never in a mirrored directory.

Nothing labels a generated file as generated — plugins/kyberforge/skills/forge/SKILL.md is byte-identical to its .apm/ original, with no marker in either. Check the path before you edit. An edit to the mirror is discarded by the next sync and is reported as drift by the check-plugin-content-sync pre-push hook, which is the earliest anyone finds out. Details in docs/spec/architecture.md.

Prefer plugin skills over raw shell

This repo dogfoods its own plugins. Before shelling out to git, gitea, or lint tooling directly, check whether an installed skill already owns the operation — it usually does:

  • Commits, branches, history, worktrees, remotes → git-commits, git-branches, git-history, git-worktrees, git-remotes
  • Pre-commit hook install/config/troubleshooting → pc-run / pc-author
  • Issues, PRs, labels, milestones → gitea-issues, gitea-prs, gitea-labels-milestones; also gitea-branches, gitea-files, gitea-releases, or gitea-workflow when the domain is ambiguous
  • Vale prose linting → vale-config / vale-run
  • This repo's own AGENTS.md → agentsmd-author / agentsmd-audit

Names are unnamespaced. Under the old claude plugin install these were git:git-commits, kyberforge:skill-audit, and so on; apm install deploys each skill to .claude/skills/<name>/ as a plain project skill, which has no plugin prefix to carry. The <plugin>: form no longer resolves here — it still does in any project that installs holocron natively, so a skill body written for both audiences should name the bare skill. Same for agents: git-orchestrate, not git:git-orchestrate.

Fall back to raw shell only when no skill covers it.

Setup and testing

  • Run apm install to deploy this repo's own skills and agents into .claude/skills/ and .claude/agents/. Both are gitignored install output, not authoring source — plugins/<name>/.apm/ remains the only place to edit. The six dependencies in root apm.yml resolve from the holocron remote, unpinned against the default branch, so a .apm/ edit is not visible to the running session until it is pushed and apm update re-runs (apm install deploys from apm.lock.yaml and does not re-resolve refs). Needs the network, and needs apm_modules/ (which it materializes) left gitignored. apm install also configures the obsidian MCP server into the repo's .mcp.json, carried over from plugins/bin/.mcp.json.
  • Do not add repo-owned keys to .claude/settings.json. apm treats that file as its own deployed artifact: apm audit --ci replays the install into a scratch tree and diffs, so anything apm would not have written there — an enabledPlugins block, a real hooks entry — is permanent drift that fails the apm-audit-ci pre-push hook. Its committed content is whatever apm last wrote — {"hooks": {}} until kyberforge's SessionStart hook lands there, after which the merged hook entry is apm's output and belongs in the commit (ADR-0019). What does not change is that nothing repo-authored goes in the file. A hook you want in this repo is authored in plugins/<name>/.apm/hooks/ and deployed by apm, never hand-written here. Machine-specific settings go in the gitignored .claude/settings.local.json, which apm does not deploy and the replay does not compare; shared enforcement belongs in .pre-commit-config.yaml.
  • Keeping the install current is automatic but not free. Because the six dependencies are unpinned, deployed skills go stale whenever anyone merges. kyberforge ships a SessionStart hook that runs apm outdated at startup (~0.7s) and, when something is behind, runs apm update --yes and asks the host to re-scan skills (~10.4s). That rewrites apm.lock.yaml, so an unexplained modification to it after opening a session is expected, not a bug — commit or discard it deliberately. Note apm install alone will not pick up remote changes; it deploys from the lock. apm update is the command that re-resolves refs.
  • Install git hooks via pc-run, wiring all three stages — this repo's .pre-commit-config.yaml has no default_install_hook_types, so a plain install silently skips commit-msg (Conventional Commits) and pre-push (the 13-hook gate described below).
  • Install the apm CLI — four pre-push hooks shell out to it: apm-marketplace-check, apm-audit-ci, apm-pack-check-clean, and check-plugin-content-sync (via scripts/sync-plugin-content.sh, which wraps apm pack). apm-marketplace-check and apm-pack-check-clean are bare apm … hook entries and apm-audit-ci is a bash -c loop calling apm once per package, so without it the push dies with an unhelpful "command not found". Use apm-install, or curl -sSL https://aka.ms/apm-unix | sh; verify with apm --version.
  • Install jq — required by scripts/check-manifests.sh and scripts/sync-plugin-content.sh, both pre-push. These at least fail loudly (Error: jq is required but not installed).
  • Install the vale binary — required by the vale-audit-prefilter-skill/-agent pre-commit hooks. Their files: patterns are .apm/-scoped: ^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$ and ^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$. Only the authoring source triggers them — a SKILL.md in the generated mirror matches neither pattern, so prose findings surface only when you edit the file you are supposed to be editing. Without the binary the hooks fail with a bare "command not found" and no install pointer. brew install vale (macOS), snap install vale (Linux), choco install vale (Windows), or see https://vale.sh/docs/vale-cli/installation/. No vale sync needed — the Kyberforge styles are committed under plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/, not downloaded packages (see ADR-0014).
  • vale is also a pre-push dependency, not only pre-commit. check-vale-style-sync runs six glob-coverage probes by invoking vale --config — they are the only assertions in it that catch a .vale.ini glob typo, the failure mode where every text-level check stays clean while vale lints zero files. Missing vale is therefore a hard failure there. The opt-out is CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1, and it is not SKIP=: the hook still runs and still asserts everything verifiable from file text, but the six probes do not, and its summary says so explicitly — Vale style sync check passed (text-level only, vale unavailable): … 0 glob probe(s) verified. Use it only on a machine that genuinely cannot install vale, and read that summary line as "the glob axis was not checked", not as a pass.
  • Run bash tests/run-tests.sh before considering any change done — it runs every test-*.sh script in the repo plus the bats suite (--bats-only for just bats). First run auto-initializes the bats submodules; no manual git submodule update needed.
  • A suite that exits 77 because a dependency is missing is reported as SKIPPED, and does not fail an ad-hoc run. The pre-push hook invokes the same script as --strict (RUN_TESTS_STRICT=1 is equivalent), where a skip does fail the push: at pre-push a skip means one of the dependencies above is absent on this machine, so the gate would otherwise report success having run fewer suites than it appears to. Without vale, for instance, three suites skip (test-check-vale-style-sync.sh, test-vale-hooks-consumer.sh, test-vale-wrap.sh) and the strict failure names each one and what to install.
  • tests/run-bats.sh derives the set of .bats files it expects from git ls-files, so a .bats file deleted from the worktree but still tracked in the index fails the run rather than silently shrinking the suite. Remove one with git rm (or stage the deletion) when the removal is intentional; an untracked new .bats file is picked up and needs no ceremony. Both discovery walks (tests/run-bats.sh and tests/run-tests.sh) exclude apm_modules/: apm install materializes a full copy of every plugin there, and running a dependency's copy of a .bats file breaks its relative path to the bats helpers — 167 spurious failures before the exclusion landed.
  • Pushing runs 13 repo-defined pre-push hooks, not just the test suite — run-tests and check-manifests, plus generated-content drift gates (check-plugin-content-sync, check-marketplace-mirror-sync, check-vale-style-sync, check-scope-walkup-sync), artifact validators (check-apm-agents-valid, which runs agent-audit's validate.sh over every real plugins/*/.apm/agents/*.agent.md), apm's own gates (apm-marketplace-check, apm-audit-ci, apm-pack-check-clean), host validators (validate-plugins, validate-marketplace, both needing the claude CLI), and check-release-needed. Run pre-commit run --hook-stage pre-push --all-files locally — one command, the whole gate. That command reports 15, not 13: pre-commit's own meta hooks, check-hooks-apply and check-useless-excludes, declare no stages: and so run at every stage including this one.
  • apm-audit-ci runs apm audit --ci once per manifest — the root one and each of the six plugin packages — because the root-only invocation audits the marketplace manifest and nothing else, and apm-pack-check-clean does not parse plugin dependencies: blocks either (verified: a malformed one passes apm pack --check-versions --check-clean --dry-run and fails apm audit --ci in that package's directory). It verifies two things and claims no more: each apm.yml parses as a valid APM manifest, and any package declaring dependencies has a consistent apm.lock.yaml. It does not enforce an org policy — apm discovers one from the git remote and only understands github.com and Azure DevOps, so against this repo's self-hosted Gitea remote it prints No org policy found at unknown; enforcement skipped. Do not "fix" that with policy.fetch_failure_default: block in apm.yml: it was tested and rejected, because with no reachable policy source it makes the hook exit 1 on every push forever.
  • check-apm-agents-valid derives its expected agent-file set from git ls-files (same pattern as tests/run-bats.sh), so an agent file deleted from the worktree but still tracked fails the run, and discovering zero agent files is an error rather than a pass. An untracked new agent file is still validated — the derivation is one-directional on purpose, so uncommitted work is not blocked but also cannot bypass the gate.
  • Two pre-push hooks need the network, for one shared reason: root apm.yml's marketplace.packages[] contains exactly one remote entry (mattpocock-skills, source: mattpocock/skills), and resolving it needs a git ls-remote. apm-marketplace-check resolves every entry and is always_run, so it fails with No cached refs (offline). apm-pack-check-clean (apm pack --check-versions --check-clean --dry-run) re-resolves the same entry and fails with Error: Git network timeout during ls-remote. Pinning the entry to an exact version does not remove the call — an exact pin still ls-remotes. --offline rescues neither. To push without a network, skip both using pre-commit's own mechanism: SKIP=apm-marketplace-check,apm-pack-check-clean git push. Skip those two alone — verified under unshare -rn, the other eleven pre-push hooks pass offline because they are real local checks, and adding one of them to SKIP disarms it silently. apm-audit-ci calls apm too but stays local: its org-policy discovery resolves nothing on this remote before any network call, so it does not join the pair above.
  • Author commits with git-commits — it validates Conventional Commits (enforced at commit-msg) for you.

Key documents

Read CONTEXT.md at the start of every session in this repo.

Read these on demand:

  • docs/spec/architecture.md — current directory structure, install pipeline, provider model
  • docs/adr/ — architectural decisions; read before answering design questions or proposing structural changes
  • docs/ai-constitution.md — full governance evidence base; read when a governance decision needs justification
  • docs/research/ai-coding-factory/ai-coding-factory-principles.md — factory design rationale; read when implementing, auditing, or reviewing skills or factory structure
  • docs/notes/factory-integration-decisions.md — decisions from the factory integration grill; read when making skill authoring or factory design decisions
  • Governance rules are always in effect — core/instructions/governance.md (agent rules); docs/research/governance_principles/CONTROLS.md