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
33 KiB
name, description
| name | description |
|---|---|
| AI Development Repo | Domain language and decisions for the global AI development config repository |
Context
Principles
CLAUDE.md index model
AGENTS.md is the source of always-on universal rules (provider-agnostic). providers/claude-code/CLAUDE.md is a thin adapter: it imports ~/.agents/AGENTS.md via @~/.agents/AGENTS.md and appends Claude Code-specific additions (@import for governance.md, content index). Deployed to ~/.claude/CLAUDE.md via install.sh. Context size is kept minimal — only what is needed every session is loaded upfront; detailed content is pulled on demand. See ADR-0003.
Instruction file format
core/instructions/<topic>.md files are plain markdown — no frontmatter, no schema. The agent decides when to read each file based on task context and the content index label in providers/claude-code/CLAUDE.md. Frontmatter is deferred until there is evidence that agents are loading the wrong files in practice.
Repo/gitea as source of truth
All project state, decisions, context, and working conventions live in this repo or Gitea. External memory systems should not be used for this project — they create a split-brain risk where cached state diverges from the repo. At the start of every session, read CLAUDE.md, CONTEXT.md, and docs/VISION.md. Everything needed to orient is here.
Before answering any design or architecture question, check for existing decisions: docs/adr/ (hard architectural decisions).
Glossary
Management Application
A separate product (separate repo) for browsing, editing, and configuring AI development configs through a proper product UI. Git is the persistence layer, invisible to the user. The app is repo-agnostic — it works with any git repo that follows these conventions. This repo is the canonical default content (the official starter). See docs/VISION.md for the phased roadmap.
Skills
Reusable slash commands for AI coding tools, defined as SKILL.md files following the Agent Skills open standard. Authored at plugins/<plugin-name>/.apm/skills/<skill-name>/SKILL.md and reaching a host by one of two install paths: apm install, which deploys the skill directory to .claude/skills/<skill-name>/ (this repo's own path — see "apm-consumed install"), or claude plugin install <name>@<marketplace>, which caches the whole plugin (still supported for external consumers). Skills are self-contained — they cannot reference files outside the plugin directory after install-time caching. The two paths name skills differently: apm deploys a plain project skill (skill-audit), a plugin install namespaces it (kyberforge:skill-audit).
Preload tax
The always-on context cost of every installed skill's name + description, which sit in the agent's context from the first token of every session whether or not the skill is invoked. Measured 2026-08-14 against base commit f9b919d at 23,427 chars (~5,900 tokens) across 39 skills, plus 1,325 chars for 4 agents. Method, so it can be re-run: sum len(name) + len(description) over each plugins/*/.apm/skills/*/SKILL.md frontmatter with > block scalars folded to the value the host loads, at ~4 characters per token. Non-routing frontmatter (metadata.source_keys, category, version) is not part of it — the model-visible skill listing carries only name and description, which supersedes LESSONS.md:63 on this host. Bodies are not part of it either; they are charged on invocation.
Skill context contract
The authoring rules that hold the preload tax and body size down, set by ADR-0020. A description carries a trigger clause, at most one capability clause, and a boundary clause of the form Not <thing> → <skill-name> naming a resolvable target — nothing else. "Resolvable" is decided by walking up from the file being checked to an authoring root — the nearest ancestor holding plugins/*/.apm/{skills,agents}, falling back to the nearest .git, in two passes so a nested .git cannot outrank a real monorepo root. The universe is then every skill and agent under <root>/plugins/*/ (sibling plugins resolve against each other, which is what a monorepo means), plus the checked file's own apm package and that package's own declared dependencies.apm. The root manifest's dependency list is never consulted, and no plugin here declares a cross-plugin apm dependency. Deployed .claude//.agents/ trees count only when there is no authoring root at all — the consumer case. The property this buys is that one commit gets one verdict: those trees are gitignored apm install output, so resolving through them made the same commit report 2 dangling targets on a developer machine and 6 on a fresh clone, which a gate shipping hot with no baseline cannot do. A ${BASH_SOURCE}-relative repo root is the other half of the same defect and is gone — it leaked this repo's 39-skill universe into consumer repos running the hook through pre-commit. A missing boundary clause is a SUGGESTION rather than a failure, for skills and agents alike — some skills genuinely have no near-miss sibling. A missing or empty description is the opposite: a hard FAIL in all three validators, because a gate that merely declines to measure the one preloaded field reports green. Capability enumeration, output formats, and composition notes ("composes X rather than duplicating Y") belong in the body or README.md; a description that summarises workflow is a correctness hazard, not just a cost, because agents act on it instead of reading the body. Sizes are two-tier and sit below the agentskills.io spec limits, which stay unchanged as conformance backstops: description 250 SUGGESTION / 400 FAIL (spec 1,024); body 600 SUGGESTION / 900 FAIL (spec 2,770 words / 500 lines). Conflating the quality gate with the spec ceiling is what let skill-author and agent-author grow to within twelve words of 2,770.
Dispatch body
The body pattern a skill with two or more mutually exclusive flows must use: the body carries only the dispatch table and the gates common to every branch, and each flow lives in its own self-contained references/ file. Named for apm-workflow (421-word body, 3,006 words of references), which arrived at it independently and is the repo's exemplar. Its absence was the characteristic defect at the time ADR-0020 was written: skill-author inlined both its create and improve flows, and agent-author carried 50-60 lines marked inapplicable by their own headers on any single run. Both were retrofitted to dispatch tables in the change that carries the ADR — skill-author went 2,623 body words to 595 and agent-author 2,582 to 616 — so they are now worked examples of the pattern rather than counter-examples of it. The 39-skill corpus at large is not: 9 bodies still exceed the 900-word FAIL (issue #99).
Hand-invoked skill
A skill reached only by typing its slash command, declared with disable-model-invocation: true. The host withholds it from the model-visible skill listing entirely, so it pays no preload tax and its description becomes human-facing text rather than a trigger list. zoom-out is the worked example: apm passes the flag through verbatim to both install paths, and the skill is absent from the router while /zoom-out still works. Choosing model-invoked vs. hand-invoked is the first question skill-author asks, because it determines whether a description needs triggers at all.
Delegation discipline
The agent-side counterpart to the dispatch body. A plugin-scope agent is a single .apm/agents/<name>.agent.md file with no sibling references/ directory, so it cannot disclose to itself — it can only delegate to skills. Its characteristic defect is therefore restatement, not length: an agent body that spells out a procedure a skill it can invoke already owns creates a second copy that drifts. agent-audit fails that, with the fix being "invoke <skill> instead". Agents take the same description gates as skills but no body word gate — a skill body competes with the caller's live conversation, an agent body becomes the system prompt of a fresh context.
Plugin
The deployable unit in the plugin marketplace. A plugin bundles one or more skills, agents, hooks, prompts, MCP servers, and optionally a bin/ directory into a single installable directory. In this repo, plugins live under plugins/<name>/, each with its own apm.yml + .apm/{skills,agents,hooks,...} — this is the authoring source of truth for the plugin's content (ADR-0015). Two categories of tracked output are compiled from that source, never hand-edited: .claude-plugin/plugin.json (Claude Code) and .github/plugin/plugin.json (Copilot CLI) via apm pack/apm compile; and, alongside them, a flat agents/, skills/, commands/, instructions/, extensions/ directory mirror at the plugin root plus a merged hooks file at hooks/hooks.json, generated by scripts/sync-plugin-content.sh — Claude Code's and Copilot's installers convention-scan only these flat paths (hooks/hooks.json is the convention path for hooks specifically; a root-level hooks.json is scanned by nothing and is deleted as stale by a sync — see ADR-0017's 2026-08-14 amendment) and have no awareness of .apm/ nesting at all, so this mirror is what actually makes .apm/ content discoverable at install time (ADR-0017). Plugins are copied to a cache on install — they cannot reference files outside their own directory. Install a plugin with claude plugin install <name>@<marketplace>, or consume it as an apm dependency (see "apm-consumed install").
Plugin marketplace
A Git repository with a marketplace.json manifest listing installable plugins. No backend, registry, or SaaS required — the Git repo is the marketplace. This repo is the holocron marketplace. The manifest at .claude-plugin/marketplace.json (read by both Claude Code and Copilot CLI) is compiled output of apm pack, generated from the root apm.yml's marketplace: block (owner, build/output config, versioning strategy, and the packages: list of installable plugins) — it is not hand-edited. See ADR-0015. .github/plugin/marketplace.json is Copilot CLI's legacy manifest path; apm has no output profile for it (only claude and codex, and codex's is a differently-shaped file at .agents/plugins/marketplace.json), so scripts/sync-marketplace-mirror.sh keeps it byte-identical to .claude-plugin/marketplace.json, checked at pre-push. Each listed package's source: still points at that plugin's own plugins/<name>/ root, not at an apm pack build artifact — which is why that root also carries the flat agents//skills//commands//hooks/hooks.json content mirror described under "Plugin" (ADR-0017): without it, an install from this marketplace finds a valid manifest but no discoverable content.
apm-consumed install
How this repo installs its own plugins, as of 2026-08-14: not claude plugin install <name>@holocron, but six dependencies.apm entries in the root apm.yml, each a git:/path: object against the holocron remote, deployed by apm install into .claude/skills/ and .claude/agents/. Project scope only — apm installs nothing at user scope, so the switch is contained to this repo and any other repo opts in by declaring its own dependencies. The git+path object form is deliberate over the shorter <name>@holocron marketplace alias: an alias must first be registered with apm marketplace add, which writes to ~/.apm/marketplaces.json (user scope, outside the repo), whereas the object form needs nothing beyond the committed manifest and so survives a fresh clone.
Four consequences, each load-bearing:
- Skills gain an unnamespaced name. apm deploys plain project skills, so
git:git-commitsalso answers togit-commits. The<plugin>:form has not stopped resolving here:~/.claude.jsonstill enablescore,git,gitea,kyberforge, andlintat user scope, which ADR-0018 left in place deliberately — converting them is a separate decision with a blast radius beyond this repo. Until it is taken, every skill is live under two names, which is the same "present twice under two names" outcome ADR-0018's own "Alternatives considered" rejected for keeping both install paths — reached here by leaving user scope alone rather than by adopting it as the install model. Write the bare name regardless: apm deploys it, and a repo consuming holocron through apm gets only that form. The namespaced form still resolves wherever holocron is installed natively, so cross-audience skill bodies should use the bare name. - apm owns
.claude/settings.json.apm audit --ci(anapm-audit-cipre-push hook) replays the install into a scratch tree and diffs it against the worktree, so any key apm would not have written is permanent drift. Committed content is exactly{"hooks": {}}; repo-owned settings have nowhere to live in that file. - Install output is gitignored.
.claude/skills/,.claude/agents/, andapm_modules/are all regenerated byapm install.apm.lock.yamland the generated.mcp.jsonare committed. Committing the deployed skills would add a third mirror of the same content to the two ADR-0017 already governs. - Test discovery must skip
apm_modules/. It holds a full copy of every plugin,.batsfiles included; bothtests/run-bats.shandtests/run-tests.shexclude it.
Dependencies are unpinned against the default branch, matching the autoUpdate: true the native marketplace install had. The practical cost is a round trip: an edit to plugins/<name>/.apm/ is invisible locally until it is pushed and apm install re-runs, because the dependency resolves from the remote rather than from the working tree beside it.
HITL (human-in-the-loop)
Agent pauses before a consequential action; human approves before execution. Required for irreversible or high-stakes actions (architecture changes, production deployments, security configuration). The agent drafts the change plan and waits — it does not proceed autonomously. Contrast with HOTL.
HOTL (human-on-the-loop)
Agent acts; human monitors and can intervene after the fact. Acceptable for low-stakes, bounded, reversible actions where the cost of pausing for approval exceeds the blast radius of an error. The distinction between HITL and HOTL must be explicit and documented — defaulting to HOTL for convenience is not acceptable.
Sycophancy
The failure mode where RLHF-trained models prioritise approval over accuracy. Treated as a first-class reliability risk: models change correct answers to wrong ones under user pressure in a majority of observed cases, then persist in the wrong answer. Designing against sycophancy is an explicit obligation, not a quality-of-life concern. Countermeasures: explicit pushback resistance instructions, prompting for dissent, cross-validating against independent sources. Never interpret AI agreement as AI accuracy.
AGENTS.md
The provider-agnostic always-on instruction entry point. Two files:
- Repo-level
AGENTS.md— instructions for agents working inside this repo (structure, key rules); imported by repoCLAUDE.mdvia@AGENTS.md. - Global
core/AGENTS.md— Communication and Behavior rules that apply across all projects; deployed to~/.agents/AGENTS.md; imported by~/.claude/CLAUDE.mdvia@~/.agents/AGENTS.md.
Contains always-on rules in plain markdown with no provider-specific syntax (no @import). Provider-specific files (CLAUDE.md) are thin adapters that import the relevant AGENTS.md and add only Claude Code-specific syntax. This pattern means a single source of truth can serve multiple providers without duplication. See ADR-0003.
Skill composition
A skill calling another skill by name to delegate a sub-task. The calling skill focuses on the orchestration decision ("when to do X"); the called skill owns the mechanics ("how to do X"). Established compositions: grill-me calls write-adr when a decision crystallises; implement-feature calls tdd as its implementation methodology; forge calls grill-with-docs to refine intent, classifies the target artifact type (skill / agent / plugin / marketplace entry), then routes to the matching *-author skill — which owns its own create/improve logic and, where applicable, its own inline audit closeout (skill-author runs /skill-audit, agent-author runs kyberforge:agent-audit, both in the same context as the authoring work). Reserve forge for genuinely undecided "which artifact type is this" questions — an already-fully-specified corrective edit (exact file, line, and fix already known) should call the target author skill directly instead (skill-author, apm-workflow, agentsmd-author, etc.); routing a known fix through forge's grill-and-classify layer adds unnecessary indirection and, in practice, has been observed to lose track of hard constraints handed down the chain (e.g. "don't commit yet," "edit in this worktree") because each hop re-derives instructions from a shorter brief. forge additionally runs its own independent recheck after a skill/agent route finishes: a clean-context subagent (not forked, no inherited context) re-runs the same audit skill against the finished artifact, as a distinct verification layer from the author skill's inline audit — the two can share blind spots since the inline audit runs in the same context as the work it checks. If the clean audit surfaces any unresolved finding, forge loops — re-invoke the author skill to resolve it, re-run the clean audit — until the clean audit comes back with nothing unresolved; only then is the route done. plugin-author and marketplace-author had no audit counterpart and got no recheck; their terminal check was claude plugin validate. Both were deprecated per ADR-0015, superseded by apm-workflow, and deleted entirely once issue #90 landed.
Provider-agnostic issue tracker
Skills and workflows reference "linked issue" generically rather than a specific provider. Gitea is the canonical issue tracker for this repo (see ADR-0007). "Issue" is the cross-provider term (GitHub, GitLab, Gitea all use it).
Provenance chain
The three-stage traceability record linking a skill back to its research inputs: (1) /research produces topic docs and a sources.md in plugins/<plugin>/docs/research/docs/<topic>/; (2) /skill-author reads those docs and records which sources informed which skill files in references/sources.md (including a Research doc: back-pointer to the upstream research file) and source_keys frontmatter on SKILL.md and references/*.md; (3) skill-audit validates the chain is complete and internally consistent via validate-provenance.sh. A skill with research input but no references/sources.md, or with source_keys that don't match references/sources.md slugs, has a broken provenance chain.
Bidirectional reference principle
Files that reference other files should declare those references explicitly. The referencing file carries the forward reference (e.g. content index in CLAUDE.md, references: in frontmatter). The referenced file carries a when: field describing when it is loaded. Both sides should agree — divergence signals staleness. The reverse map ("what files reference this file?") is derived by a reference scanner script, not maintained manually. This principle applies to instruction files, skills, and workflow documents.
agentsmd-author / agentsmd-audit
A skill pair in the core plugin for writing, updating, and reviewing a repo's AGENTS.md file(s) — the generic open-standard file (see the AGENTS.md entry above), including this repo's own. agentsmd-author creates/updates AGENTS.md content, supports nested monorepo placement (per the standard's nearest-file-wins precedence), and closes out by invoking agentsmd-audit inline. agentsmd-audit runs a single combined pass checking three mandatory baselines: secrets/credentials (governance.md hard prohibition — AGENTS.md is committed content), structural completeness (common-sections checklist from the agents.md spec), and accuracy/drift (do referenced commands and paths actually resolve against the repo). agentsmd-audit never inspects provider adapter files (see provider-adapter-author) — its scope is AGENTS.md content only. Chosen over folding this into kyberforge because kyberforge's scope is meta-tooling for the holocron marketplace itself, not generic target-repo documentation; core is the intended home for cross-cutting, repo-agnostic utility skills.
provider-adapter-author
A companion skill (core plugin) that detects a target repo's provider-specific instruction file (CLAUDE.md, .cursor/rules/*.mdc, copilot-instructions.md, etc.) and, where it duplicates content AGENTS.md should own, converts it into a thin adapter that imports AGENTS.md — mirroring this repo's own ADR-0002/ADR-0003 two-tier adapter pattern. Self-validates via its own bundled deterministic script (scripts/validate-adapter.sh: checks for an import reference, no duplicated headings, size threshold) rather than a separate paired audit skill — the check is mechanical, so a script suffices per governance.md's "prefer deterministic code for repeatable tasks." agentsmd-author calls this skill via skill composition when it detects an existing provider file with overlapping content.
lint plugin
A standalone, repo-agnostic plugin (plugins/lint/) for configuring and running linters — not scoped to kyberforge's own meta-tooling. First linter is Vale (prose style linting), split into two skills per the git/gitea per-concern pattern: vale-config (setup — .vale.ini, StylesPath, styles) and vale-run (invoke Vale, interpret/report findings). A lint-runner agent composes these for isolated-context lint sweeps; it is report-only by instruction, not by capability — its body states "You never edit files" and "Do not edit, fix, or rewrite any flagged content", but nothing enforces that. It previously carried tools: Bash, Read, Grep, Glob, which withheld Edit outright; plugin-scope APM agents cannot express a tools: field at all (ADR-0016 — apm compile copies frontmatter verbatim to both Claude Code and Copilot, whose tools: vocabularies are incompatible, so a value correct for one harness is wrong for the other), so plugins/lint/.apm/agents/lint-runner.agent.md now declares only name/description/source_keys and inherits every tool, Edit included. ADR-0016 accepted this loss of enforcement knowingly; the restriction survives as prose the agent is expected to follow. Vale's research docs (docs/research/docs/vale/) moved from plugins/kyberforge/ to plugins/lint/ to keep the provenance chain same-plugin.
Vale audit prefilter (skill-audit / agent-audit)
Wiring Vale as a deterministic prefilter for skill-audit/agent-audit's Description dimension (ADR motivation: issue #84) is repo-specific, not part of the generic lint plugin, so it doesn't live in plugins/lint/ — but per ADR-0014 it also doesn't live at the repo root anymore. Two copies live inside plugins/kyberforge/, one per skill, since a plugin's cache-install only copies each skill's own files (no cross-skill sharing): plugins/kyberforge/.apm/skills/agent-audit/assets/vale/ is canonical (.vale.ini plus a custom Kyberforge style covering description-opener banning ("This skill/agent..."), vague-capability wording ("helps with", "utilize", ...), and generic "see references/ for details" padding — and a KyberforgeCopilot style scoped only to .agent.md files for the Copilot-only "Use proactively has no effect" check), and plugins/kyberforge/.apm/skills/skill-audit/assets/vale/ is a smaller duplicate (Kyberforge only, scoped to SKILL.md) kept in sync by scripts/check-vale-style-sync.sh (pre-push). A root-level .pre-commit-hooks.yaml exposes both copies (plus skill-size-check) so any external repo can enforce the same rules via repo: <this-repo-url>, rev: <tag> in its own .pre-commit-config.yaml — pre-commit clones the pinned rev into its own cache, independent of whether Claude Code or the kyberforge plugin is installed at all, and the same mechanism covers CI (pre-commit run --all-files). This repo's own vale-audit-prefilter-skill/-agent pre-commit hooks consume the identical plugin-bundled copies via repo: local (not a third root copy, and not a pinned self-reference — a pinned self-reference would lint working-tree edits against the last tagged release rather than the change being made). Every rule is level: error and every alert is a FAIL — no ignorable tier, same as shellcheck, the test suite, and conventional-pre-commit. Graded severities do not work here: Vale's exit code keys on error alerts alone, so warning/suggestion rules exit 0 and pre-commit swallows the output of a passing hook, leaving them invisible and blocking nothing. MinAlertLevel and --minAlertLevel are correspondingly absent from .vale.ini and the hook, being no-ops under this model. Vale covers the pattern-matchable sub-checks named in issue #84 (imperative opener, vague filler, Use proactively, generic reference-pointer padding) plus, per ADR-0013, one body-wide prose-pattern check ("There is/are" sentence openers) — everything else about body discipline (defaults-vs-menus, why-rationale, non-pattern-matchable judgment calls), near-miss exclusion strength, and control calibration stays LLM judgment.
Both skills' Step 1, and the vale-audit-prefilter-skill/-agent pre-commit hooks, call each copy's own scripts/vale-wrap.sh rather than vale directly — a workaround for a confirmed Vale 3.15.2 limitation (see vale-config's Gotchas): text.frontmatter.description silently stops matching on most — not all — multi-line descriptions. Verified by reproduction, not assumed: > folded scalars, plain (unquoted) continuation lines, and single- or double-quoted multi-line scalars all yield 0 alerts and exit 0 on a deliberately-bad fixture, while a | literal block spanning the same 2+ lines lints normally (alerts fire, exit 1). The wrapper flattens those three broken forms to one physical line in a scratch copy (padding with blank lines so every other line number is unchanged) before handing off to real vale; | literal blocks and single-line descriptions pass through untouched, already linting correctly. The plain and quoted forms previously passed silently — unflattened and unmatched — so a bad description in either sailed through the prefilter. Handed no --config at all, the wrapper falls back to its own sibling assets/vale/.vale.ini, located from ${BASH_SOURCE[0]} rather than from the cwd — which is why both manifests' entry: is now the bare script path with no argument after it. pre-commit prefixes only entry[0] with the hook-repo clone path (cmd = (prefix.path(cmd[0]), *cmd[1:])), so every later argument resolves against the consuming repo's root: a --config in .pre-commit-hooks.yaml pointed at a path no consumer has and hard-failed every external run with E100 [--config] Runtime error. .pre-commit-config.yaml drops the argument too, deliberately keeping the two entries identical — the local repo: local hook resolved its --config correctly only because the consuming repo was this repo, and that divergence is why three review rounds exercised a path no external consumer takes and missed the defect. An explicit --config still wins, in all three argv forms (--config X, --config=/abs, --config=rel), and a relative one still resolves against the caller's cwd, matching bare vale, not the repo root. Both audit skills' Step 1 now passes no --config either: it resolves the script relative to the skill's own directory so the call works from an installed plugin cache, but a relative --config alongside it would still resolve against the cwd, yielding E100 Runtime error ... does not exist and exit 2 — which both skills' fallback misreads as "vale unavailable" and silently downgrades to full LLM judgment. tests/test-vale-wrap.sh regression-tests this against skill-audit's copy specifically (its fixtures are all SKILL.md-shaped, and only skill-audit's .vale.ini has that glob section). Each .vale.ini's section globs are path-agnostic ([**/SKILL.md] for skill-audit's copy; [**/agents/*.md]/[**/*.agent.md] for agent-audit's) and do no scoping on their own: Vale's * crosses /. Scoping comes from each pre-commit hook's own files: regex and from the audit skills passing one explicit file per invocation. The two manifests scope differently on purpose: this repo's .pre-commit-config.yaml pins its own layout — ^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$ for -skill, ^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$ for -agent — while the shipped .pre-commit-hooks.yaml stays layout-agnostic for external consumers whose skills live anywhere, using (^|/)SKILL\.md$ and (^|/)agents/[^/]+\.md$|\.agent\.md$. Both manifests split the prefilter into two hooks precisely because one combined hook pointed at only one copy would silently 0-file-skip the other file type. A SKILL.md outside plugins/ (e.g. project-scope .claude/skills/foo/SKILL.md) still matches [**/SKILL.md] and gets linted normally — the globs constrain filename shape, not location. Vale reports 0 files only when the path it is handed matches no glob section at all: a differently-named file, or a directory argument holding nothing that matches. That run prints ✔ 0 errors ... in 0 files. and exits 0, indistinguishable from a clean pass, so both audits treat a 0-file Vale run as NOT RUN and fall back to full LLM judgment.
This scope expands per ADR-0013: one cherry-picked low-noise write-good/alex rule landed in styles/Kyberforge, Kyberforge.SentenceOpenerThereIs (22 held-out hits, both in-corpus hits clean rewrites, zero suppressions). A second, Kyberforge.VagueQualifier, was cherry-picked and then deleted: 2 hits across the skill/agent corpus as it stood at the time of that measurement (2026-08-08, before the .apm/ restructure), one marginal and one an unfixable false positive (caveman/SKILL.md quotes of course as an example of filler — a mention, not a use) that forced the repo's only Vale suppression comments. A third, Kyberforge.CompositionNote, landed with ADR-0020 and bans architecture and composition prose from a description; it is level: error like the rest, and it currently fires 10 times across gitea-issues, gitea-labels-milestones, gitea-prs and gitea-workflow, so pre-commit run --all-files is red on prose as well as on size until issue #99 lands. Also new is a sibling pre-commit hook, skill-size-check (scripts/skill-size-check.sh), which carries two independent gate families that must not be conflated (see "Skill context contract"). The agentskills.io spec backstop is MAX_LINES=500 and MAX_WORDS=2770, both inclusive and both counting the whole file including frontmatter (2,770 is a word-count proxy for the 5,000-token limit, calibrated to the densest prose measured in this repo — 1.81 tokens per word — so even a worst-case SKILL.md at the ceiling stays under 5,000 tokens; it is not a percentile of the corpus). ADR-0020 adds a context budget measured differently: description characters 250 SUGGESTION / 400 FAIL, body-only words 600 SUGGESTION / 900 FAIL, plus deterministic checks that every boundary routing target resolves, that a body's named references/<file>.md all exist, and — SUGGESTION-tier — that a boundary clause is present at all, that ## Gotchas holds at most five entries, and that it stays under 25% of the body. skill-audit/scripts/validate.sh and agent-audit/scripts/validate.sh hold their own copies of the shared constants and tests/test-skill-size-check.sh asserts the copies agree, so a SKILL.md can no longer pass its own audit yet be blocked by the commit hook. Agents take the description gates and no body word gate. python3 and PyYAML are hard requirements — the earlier hand-rolled frontmatter fallback is gone, because a fallback that silently mis-parses a scalar shape reports a vacuous pass. Scoped to ^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$ only, same as vale-audit-prefilter-skill, so it never lints docs/research/examples/ reference skills. It's also exposed in the root-level .pre-commit-hooks.yaml as kyberforge-skill-size-check — it has no external asset dependency, so it needed no relocation, only exposure to external consumers. File scope (SKILL.md + agent files) and enforcement model (rules land directly in styles/Kyberforge, blocking immediately, no trial tier) stay unchanged; governance.md/CONTROLS.md were evaluated and excluded as rule sources (nothing prose-pattern-matchable to mine). House convention: banned phrasing that must be mentioned rather than used goes in backticks or a fenced code block — Vale skips code spans and fences, so no suppression is needed; inline <!-- vale Rule = NO --> (HTML-comment form; the MDX {/* */} form does not work in plain Markdown) is the fallback only where backticking is impossible.
LESSONS.md
Long-loop feedback log for patterns observed across sessions. Three or more entries on the same pattern graduate to the relevant standing file (e.g. a coding convention, a governance rule). Updated by the session-handoff skill or directly by the human. Lives at the repo root.