The ADR was written against base commit `f9b919d` and then not updated as the implementation moved, so several of its numbers were measuring one thing and being read as another — the exact conflation the ADR exists to stop, reproduced inside it. Corrections, all reproducible now that each figure states its method: - The preload tax is 23,427 chars / ~5,900 tokens, not 23,612 / ~6,200. - `MAX_WORDS=2770` is a density proxy for the agentskills.io ~5,000-token ceiling, not "2× p90". Neither percentile reaches it: 2× the body-only p90 is 2,698 and 2× the whole-file p90 is 3,052. Reading it as a percentile pairs a whole-file gate against a body-only distribution. - `apm-workflow` is a 421-word body; 554 is its whole-file count. `skill-author` and `agent-author` were 2,623 and 2,582 body words — 2,760 and 2,758 whole-file, which is where "within twelve words of the gate" comes from. Two numbers for one file is the point, and only one of them is what either gate measures. - Every `file:line` citation now says it resolves against `f9b919d`, since this change rewrites most of the cited files. Three things the ADR asserted that no validator implemented are now filed by tier in an exhaustive enforcement table — deterministic, prose-pattern, or auditor judgment — because a rule filed under "Enforcement" that nothing enforces is the failure mode this ADR is most exposed to. The Gotchas entry count moves to SUGGESTION to match the script; the paraphrase FAIL is marked as an auditor's, since semantic equivalence is not pattern-matchable. Two gaps recorded rather than quietly left: - The agent body-gate exemption lives in `agent-audit`'s validator and in the `skill-size-check` hook's `SKILL.md`-only `files:` pattern — *not* in `scripts/skill-size-check.sh`, which measures whatever path it is handed and today reports 900-word body FAILs on `git-orchestrate` (933), `gitea-orchestrate` (1,199) and `apm-orchestrate` (1,080). Agents escape by file pattern, not because the script knows the difference, so widening that pattern would silently enforce a gate this ADR declines to set. - The `skill-audit`/`agent-audit` merge is deferred to #101. This change made the split deeper, not shallower: the dispatch retrofit took them from 3 and 4 reference files to 7 and 8, and their two same-named `description-quality.md` files now differ on 100 of ~120 lines after normalising skill/agent. The merge reopens ADR-0008 and touches every call site in `skill-author`, `agent-author` and `forge`, so it is its own change. #100 carries the dangling-target fixes. AGENTS.md and CONTEXT.md take the same corrections plus the two live setup changes: PyYAML is now a hard requirement rather than an optional accelerator (a fallback that mis-parses an unfamiliar scalar shape reports a clean pass on a file it never measured), and `.claude/settings.json`'s `pretty-format-json` exclusion is documented as load-bearing rather than as a tidy-up candidate. LESSONS.md's autofix entry is corrected on its own provenance, which it got wrong in both directions. `git log --date=iso` puts the introducing commit at 18:47 and the fix at 21:54 — three hours, not "weeks" — and `git branch -a --contains` puts the introducing commit on this branch only, not on main. It was manufactured inside the same PR that diagnosed it. The added lesson is that "pre-existing" is a claim about history and history is queryable: a defect found while working on a branch feels inherited, and the feeling is not evidence. Refs: ADR-0020, #99, #100, #101
23 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: rootapm.ymldeclares all six asdependencies.apmgit+path entries against the holocron remote, andapm installdeploys them into.claude/skills/and.claude/agents/(both gitignored). External consumers can still install natively viaclaude plugin install <name>@holocron— the marketplace manifests are unchangedproviders/claude-code/— Claude Code adapter (deployed to~/.claude/viainstall.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.shgenerates the flatplugins/<name>/{skills,agents,commands,instructions,extensions}/directories and the mergedplugins/<name>/hooks/hooks.json(ADR-0017)apm packgenerates both per-plugin manifests —plugins/<name>/.claude-plugin/plugin.jsonandplugins/<name>/.github/plugin/plugin.json— and two of the three root marketplace manifests:.claude-plugin/marketplace.json(apm'sclaudeoutput profile) and.agents/plugins/marketplace.json(itscodexprofile, a differently-shaped file) (ADR-0015)scripts/sync-marketplace-mirror.shgenerates 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,claudeandcodex(documented inplugins/kyberforge/.apm/skills/apm-workflow/references/marketplace.md). The mirror is a byte-identical copy of.claude-plugin/marketplace.json, gated by thecheck-marketplace-mirror-syncpre-push hook. Do not expectapm packto 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; alsogitea-branches,gitea-files,gitea-releases, orgitea-workflowwhen the domain is ambiguous - Vale prose linting →
vale-config/vale-run - This repo's own AGENTS.md →
agentsmd-author/agentsmd-audit
Use the bare, unnamespaced names above. 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 has not stopped resolving here, though — ~/.claude.json still enables core, git, gitea, kyberforge, and lint at user scope, and ADR-0018 left those native installs in place on purpose, converting them being a separate decision with a blast radius beyond this repo. Every skill is therefore live under both names right now, and a working gitea:gitea-prs is the user-scope copy answering — not evidence that the apm install or this file is broken, and not something to "fix". Prefer the bare name anyway: apm deploys it, an external consumer installing holocron through apm gets it, and it is the form that survives those user-scope installs eventually being converted. The namespaced form also still resolves 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 installto 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 rootapm.ymlresolve from the holocron remote, unpinned against the default branch, so a.apm/edit is not visible to the running session until it is pushed andapm updatere-runs (apm installdeploys fromapm.lock.yamland does not re-resolve refs). Needs the network, and needsapm_modules/(which it materializes) left gitignored.apm installalso configures theobsidianMCP server into the repo's.mcp.json, carried over fromplugins/bin/.mcp.json. - Do not add repo-owned keys to
.claude/settings.json. apm treats that file as its own deployed artifact:apm audit --cireplays the install into a scratch tree and diffs, so anything apm would not have written there — anenabledPluginsblock, a realhooksentry — is permanent drift that fails theapm-audit-cipre-push hook. Its committed content is whatever apm last wrote, which today is the mergedSessionStartentry for kyberforge'scheck-apm-current.sh— apm's own output, and it 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 inplugins/<name>/.apm/hooks/and deployed by apm, never hand-written here. The file is also excluded frompretty-format-jsonin.pre-commit-config.yaml— the sixth and last alternation in thatexclude:pattern, and the only one there for a reason other than "generated manifest". Mind which number you are quoting: six alternations, expanding to sixteen real files (3 root marketplace manifests, 2 per plugin × 6 plugins, plus this one).pretty-format-json --autofixsorts object keys while apm emits insertion order, so leaving the file in that hook's scope rewrites apm's output on the way into every commit andapm audit --cithen reports permanent drift on a file with an emptygit diff. Do not tidy it out of that list; it is load-bearing (seeLESSONS.md, 2026-08-14). 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
SessionStarthook that runsapm outdatedat startup (~0.7s) and, when something is behind, runsapm update --yesand asks the host to re-scan skills (~10.4s). That rewritesapm.lock.yaml, so an unexplained modification to it after opening a session is expected, not a bug — commit or discard it deliberately. Noteapm installalone will not pick up remote changes; it deploys from the lock.apm updateis the command that re-resolves refs. - Install git hooks via
pc-run, wiring all three stages — this repo's.pre-commit-config.yamlhas nodefault_install_hook_types, so a plain install silently skipscommit-msg(Conventional Commits) andpre-push(the 14-hook gate described below). - Install the
apmCLI — four pre-push hooks shell out to it:apm-marketplace-check,apm-audit-ci,apm-pack-check-clean, andcheck-plugin-content-sync(viascripts/sync-plugin-content.sh, which wrapsapm pack).apm-marketplace-checkandapm-pack-check-cleanare bareapm …hook entries andapm-audit-ciis abash -cloop callingapmonce per package, so without it the push dies with an unhelpful "command not found". Useapm-install, orcurl -sSL https://aka.ms/apm-unix | sh; verify withapm --version. - Install
jq— required byscripts/check-manifests.shandscripts/sync-plugin-content.sh, both pre-push. These at least fail loudly (Error: jq is required but not installed). - Install
python3— required byscripts/skill-size-check.sh, theskill-size-checkpre-commit hook. It measures the foldeddescriptionvalue: most descriptions here are>-block scalars, so a regex over the raw lines measures indentation and newlines instead of the value. Missing it fails the hook with an install pointer rather than skipping the ADR-0020 checks, which would be a vacuous green. In practice it is already present — pre-commit is itself a Python application. PyYAML is a hard requirement too, not an optional accelerator: the hand-rolled fallback frontmatter reader has been removed, because a reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file it never measured, which is the exact vacuous-green failure thepython3check exists to avoid.pip install pyyamlif the hook reports it missing. - That hook enforces two independent gate families over
plugins/*/.apm/skills/*/SKILL.md, and neither replaced the other. The agentskills.io spec backstop is unchanged: 500 lines and 2,770 words, counted over the whole file including frontmatter. ADR-0020 adds a context budget measured differently —description250 chars SUGGESTION / 400 FAIL (it is preloaded into every session whether the skill fires or not), body-only word count 600 SUGGESTION / 900 FAIL (everything after the frontmatter's closing---), a missing, valueless ornulldescription:(a hard FAIL, not a skip — a gate that declines to measure the one preloaded field reports green), every boundary-clause routing target resolving to a real skill or agent, and everyreferences/<file>.mda body names actually existing. Target resolution walks up from the file being checked to an authoring root — the nearest ancestor holdingplugins/*/.apm/{skills,agents}, falling back to the nearest.git, in two passes so a nested.gitcannot beat a real monorepo root. The universe is then every skill and agent under<root>/plugins/*/, plus the checked file's own apm package and whatever that package declares in its ownapm.ymldependencies.apm; the root manifest'sdependencies:block is not read, and no plugin here declares a cross-plugin apm dependency. Deployed.claude//.agents/trees are consulted only when no authoring root exists — the consumer case. That matters because those trees are gitignoredapm installoutput: resolution used to reach the four cross-plugingitea-*→git-*targets through.claude/skills/alone, so the same commit measured 2 dangling targets on a developer machine and 6 on a fresh clone. It no longer does — verified by running the hook over a tree holding onlyplugins/and the rootapm.yml, which reports findings identical to the working tree (26 description / 9 body / 2 dangling / 0 missing references / 58 SUGGESTIONs). Three further checks are SUGGESTION-only: a description with no boundary clause at all, a## Gotchassection with more than five entries, and a## Gotchassection over 25% of the body. A file can sit well inside one family and fail the other. The hook isverbose: trueso the SUGGESTION tier is audible — pre-commit prints nothing at all for a passing hook, and a SUGGESTION deliberately does not fail.skill-audit'svalidate.shholds a second copy of the four ADR-0020 constants;tests/test-skill-size-check.shasserts the copies agree. - Those ADR-0020 gates ship hot, with no baseline file. 26 of 39 descriptions and 9 of 39 bodies currently exceed their FAIL tier, so editing one of those skills for any reason means retrofitting it to the contract first — a one-line fix to
gitea-prscannot be committed until that skill complies. This is deliberate, and the retrofit is tracked as Gitea issue #99. Check where a skill stands before starting:pre-commit run skill-size-check --all-files. - A second gate ships hot alongside it, and
skill-size-checkwill not warn you about it.Kyberforge.CompositionNote— the ADR-0020 Vale rule banning composition and architecture prose from a description — currently fires 10 errors across four skills:gitea-issues,gitea-labels-milestones,gitea-prsandgitea-workflow. Every Vale rule here islevel: errorwith no ignorable tier, so touching any of those four means fixing its prose findings as well as its size findings. Scoping a retrofit offskill-size-checkoutput alone will leave you blocked at the second gate. Check both:pre-commit run --all-files. - Install the
valebinary — required by thevale-audit-prefilter-skill/-agentpre-commit hooks. Theirfiles:patterns are.apm/-scoped:^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$and^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$. Only the authoring source triggers them — aSKILL.mdin 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/. Novale syncneeded — theKyberforgestyles are committed underplugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/, not downloaded packages (see ADR-0014). valeis also a pre-push dependency, not only pre-commit.check-vale-style-syncruns six glob-coverage probes by invokingvale --config— they are the only assertions in it that catch a.vale.iniglob typo, the failure mode where every text-level check stays clean while vale lints zero files. Missingvaleis therefore a hard failure there. The opt-out isCHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1, and it is notSKIP=: 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 installvale, and read that summary line as "the glob axis was not checked", not as a pass.- Run
bash tests/run-tests.shbefore considering any change done — it runs everytest-*.shscript in the repo plus the bats suite (--bats-onlyfor just bats). First run auto-initializes the bats submodules; no manualgit submodule updateneeded. - 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=1is 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.shderives the set of.batsfiles it expects fromgit ls-files, so a.batsfile deleted from the worktree but still tracked in the index fails the run rather than silently shrinking the suite. Remove one withgit rm(or stage the deletion) when the removal is intentional; an untracked new.batsfile is picked up and needs no ceremony. Both discovery walks (tests/run-bats.shandtests/run-tests.sh) excludeapm_modules/:apm installmaterializes a full copy of every plugin there, and running a dependency's copy of a.batsfile breaks its relative path to the bats helpers — 167 spurious failures before the exclusion landed.- Pushing runs 14 repo-defined pre-push hooks, not just the test suite —
run-testsandcheck-manifests, plus generated-content drift gates (check-plugin-content-sync,check-marketplace-mirror-sync,check-vale-style-sync,check-scope-walkup-sync,check-executables-allow-sync), artifact validators (check-apm-agents-valid, which runs agent-audit'svalidate.shover every realplugins/*/.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 theclaudeCLI), andcheck-release-needed.check-executables-allow-syncis the odd one in that first group — it guards a silent failure rather than drift in generated text. apm gates a package'shooks/andbin/on an exact<package>#<version>lookup in rootapm.yml'sexecutables.allow, with no wildcard and no version-less form, so bumpingplugins/kyberforge/apm.yml'sversion:without bumping the key errors nowhere: the entry simply stops matching, kyberforge'sSessionStarthook stops deploying, and the install goes quietly stale — the failure ADR-0019 records as live. Runpre-commit run --hook-stage pre-push --all-fileslocally — one command, the whole gate. That command reports 16, not 14: pre-commit's ownmetahooks,check-hooks-applyandcheck-useless-excludes, declare nostages:and so run at every stage including this one. apm-audit-cirunsapm audit --cionce per manifest — the root one and each of the six plugin packages — because the root-only invocation audits the marketplace manifest and nothing else, andapm-pack-check-cleandoes not parse plugindependencies:blocks either (verified: a malformed one passesapm pack --check-versions --check-clean --dry-runand failsapm audit --ciin that package's directory). It verifies two things and claims no more: eachapm.ymlparses as a valid APM manifest, and any package declaring dependencies has a consistentapm.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 printsNo org policy found at unknown; enforcement skipped. Do not "fix" that withpolicy.fetch_failure_default: blockinapm.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-validderives its expected agent-file set fromgit ls-files(same pattern astests/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. Agents take the ADR-0020 description gates (agent-audit'svalidate.shholds its own copy of those two constants) and, deliberately, no body word gate: an agent body becomes the system prompt of a fresh context rather than competing with the caller's live conversation, so the 900-word FAIL does not transfer. A bats test pins that absence inagent-audit's validator — adding a body gate there contradicts the ADR rather than fixing an inconsistency. Be precise about the scope of that guarantee, though: it holds for the validator, not for the shared script.scripts/skill-size-check.shapplies its body gate to whatever path it is handed, andbash scripts/skill-size-check.sh plugins/*/.apm/agents/*.agent.mdexits 1 today with 900-word body FAILs ongit-orchestrate(933),gitea-orchestrate(1,199) andapm-orchestrate(1,080). Agent files escape only because the hook definitions filter onSKILL.md— a file-pattern accident that happens to implement the design, not the design itself. Do not "extend" that hook'sfiles:pattern to cover agents on the assumption that the script already knows the difference.- Two pre-push hooks need the network, for one shared reason: root
apm.yml'smarketplace.packages[]contains exactly one remote entry (mattpocock-skills,source: mattpocock/skills), and resolving it needs agit ls-remote.apm-marketplace-checkresolves every entry and isalways_run, so it fails withNo cached refs (offline).apm-pack-check-clean(apm pack --check-versions --check-clean --dry-run) re-resolves the same entry and fails withError: Git network timeout during ls-remote. Pinning the entry to an exact version does not remove the call — an exact pin still ls-remotes.--offlinerescues 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 underunshare -rn, the other twelve pre-push hooks pass offline because they are real local checks (check-executables-allow-synclanded after that run, but reads two local manifests and makes no network call), and adding one of them toSKIPdisarms it silently.apm-audit-cicallsapmtoo 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 atcommit-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 modeldocs/adr/— architectural decisions; read before answering design questions or proposing structural changesdocs/ai-constitution.md— full governance evidence base; read when a governance decision needs justificationdocs/research/ai-coding-factory/ai-coding-factory-principles.md— factory design rationale; read when implementing, auditing, or reviewing skills or factory structuredocs/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