Files
holocron/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md
Defame1297 1164f3abad fix(lint): make Vale prefilter portable via the plugin
skill-audit/agent-audit's Step 1 resolved vale-wrap.sh/.vale.ini via
`git rev-parse --show-toplevel`, which returns whichever repo the skill
happens to run in. Inside ai-development that works; in any external repo
that installs kyberforge@holocron as a plugin, it resolves to that repo's
own root, which has no .vale.ini — the prefilter silently fell back to
full LLM judgment. ADR-0013 named this as a deliberately deferred gap.

Vale's config/styles/wrapper now ship inside the plugin itself: a
canonical copy in agent-audit/assets/vale/ (Kyberforge + KyberforgeCopilot,
the superset agent-audit needs) and a smaller duplicate in
skill-audit/assets/vale/ (Kyberforge only) — per the no-cross-skill-path
rule already established for plugin cache-installs. Both skills resolve
these relative to their own directory, same as scripts/validate.sh
already does.

A new root .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,
independent of Claude Code entirely — the same mechanism covers CI. This
repo's own pre-commit hook now consumes the identical plugin-bundled
copies via repo: local (not a third root copy, and not a pinned
self-reference, which would lint working-tree edits against the last
tagged release instead of the change being made). Split into
vale-audit-prefilter-skill/-agent hooks after confirming, by diffing the
full corpus against both old and new config before deleting the old
files, that one combined hook pointed at only one copy silently 0-file-
skips the other file type.

scripts/check-vale-style-sync.sh guards the two copies against drift,
wired at pre-push alongside check-manifests.

ADR: 0014
2026-08-09 10:04:19 +00:00

6.9 KiB

Kyberforge's Vale prefilter ships from the plugin, with .pre-commit-hooks.yaml for external git-hook/CI enforcement

Resolves: ADR-0013's deferred "styles-portability" consequence — .vale.ini/styles/ moving out of the repo root was deliberately deferred there, not fixed. ADR-0013's other content (rule scope, level: error model, SentenceOpenerThereIs/VagueQualifier trial outcomes) is unaffected and remains in force.

skill-audit/agent-audit's Step 1 called "$(git rev-parse --show-toplevel)/scripts/vale-wrap.sh" --config "$(git rev-parse --show-toplevel)/.vale.ini" — which resolves to whichever repo the skill happens to be running in. Inside ai-development that's this repo; in any external repo that installs kyberforge@holocron as a plugin, it's that repo's own root, which has no .vale.ini or vale-wrap.sh. The prefilter silently fell back to full LLM judgment every time outside this repo — the exact gap ADR-0013 named and deferred.

Decision

Runtime (a live Claude Code session): the Vale config, styles, and wrapper script move into the plugin itself, following the no-cross-skill-path rule already established in skill-author/references/deployment-modes.md (a plugin's cache-install only copies each skill's own files; there is no plugin-level shared directory). agent-audit needs both Kyberforge and KyberforgeCopilot (it lints .agent.md files), so plugins/kyberforge/skills/agent-audit/assets/vale/ is the canonical, superset copy. skill-audit needs a second, smaller copy (plugins/kyberforge/skills/skill-audit/assets/vale/, Kyberforge only) since it cannot reference agent-audit's copy across the skill boundary. Both skills' Step 1 now resolve scripts/vale-wrap.sh/assets/vale/.vale.ini relative to their own directory, the same way scripts/validate.sh <skill-dir> already does — no new resolution mechanism, just applying the existing one consistently.

git hooks / CI outside a Claude Code session have no plugin cache and no ${CLAUDE_PLUGIN_ROOT} — a CI runner in particular is guaranteed not to have one. The mechanism that works there for any consumer, with or without Claude Code installed, is pre-commit's own hook-repo protocol: this repo now ships a root-level .pre-commit-hooks.yaml exposing kyberforge-vale-audit-skill, kyberforge-vale-audit-agent, and kyberforge-skill-size-check. Any external repo adds repo: <this-repo-url>, rev: <tag> to its own .pre-commit-config.yaml and gets all three, fully decoupled from Claude Code. CI is the identical pre-commit run --all-files call, so the same manifest covers "possibly CI" from the original ask.

This repo's own dev-time gate consumes the same plugin-bundled copies instead of a third root-level copy — per explicit instruction, this repo should be set up like any other consumer would be, not dogfood a special root-only path. The existing repo: local hook is retargeted (not removed): entry: now points at plugins/kyberforge/skills/{skill-audit,agent-audit}/scripts/vale-wrap.sh. repo: local is kept rather than switching to a pinned self-reference (repo: <own-url>, rev: <tag>) — a pinned self-reference would lint working-tree edits against the last tagged release, not the change actually being made, which is wrong for the repo that is the source of the hook. This mirrors standard practice among hook-author repos (pre-commit's own pre-commit-hooks, shellcheck-py): repo: local for self-consumption, .pre-commit-hooks.yaml for everyone else, same underlying files and commands either way.

One hook per file-scope, not one combined hook. The old root .vale.ini had both the [**/SKILL.md] and [**/agents/*.md]/[**/*.agent.md] glob sections in a single file, so one pre-commit hook covered both. Splitting the config into two skill-scoped copies means a single hook entry pointed at only one copy would silently 0-file-skip the other file type. Both the local .pre-commit-config.yaml hooks and the external-facing .pre-commit-hooks.yaml therefore define separate -skill/-agent hook IDs, each with a files: regex matching exactly what its target copy's glob covers. (Confirmed empirically before deleting the root files: retargeting a single hook at agent-audit's copy silently scanned 0 SKILL.md files.)

Vale's StylesPath resolves relative to the .vale.ini file's own location, confirmed against docs.vale.sh/keys/stylespath — so --config <path-into-plugin>/.vale.ini correctly finds that ini's sibling styles/ regardless of the caller's cwd, with no extra path-juggling needed beyond what vale-wrap.sh already does for its cwd-relative --config/file-argument handling.

A sync-check catches drift between the two copies. scripts/check-vale-style-sync.sh diffs scripts/vale-wrap.sh and assets/vale/styles/Kyberforge/ between skill-audit and agent-audit (not .vale.ini — those legitimately differ, scoped to different glob sections), wired at pre-push alongside check-manifests. .vale.ini itself isn't diffed since divergence there is by design.

External .pre-commit-hooks.yaml consumers pin rev: to a tag, not a commit SHA. This repo had no tags before this change; going forward, a vX.Y.Z tag is cut whenever hook-relevant files change, matching how every other repo: entry in this repo's own .pre-commit-config.yaml already pins (v2.4.0, v8.21.2, ...).

Considered options

Keep a third root-level copy, dogfooded specially (rejected). Simpler in that this repo's own hook wouldn't need retargeting at all. Rejected on explicit instruction: this repo should consume the same portability path an external repo would, not carve out a special root-only case that never gets exercised the way external consumers exercise it.

Publish styles as a hosted Vale package via Packages = <zip-url> (deferred, not rejected). Vale supports fetching a style from a direct .zip URL via vale sync, fully decoupled from Claude Code and from pre-commit's hook-repo protocol — usable by any repo, even ones that never install kyberforge at all. This is a larger, separate investment (a release/versioning pipeline for the package itself) not required to satisfy the current ask; noted here so a future reader doesn't wonder if it was overlooked.

Consequences

  • Root .vale.ini, styles/, scripts/vale-wrap.sh are deleted. Two copies remain: plugins/kyberforge/skills/agent-audit/assets/vale/ (canonical, superset) and plugins/kyberforge/skills/skill-audit/assets/vale/ (subset, Kyberforge only).
  • plugins/kyberforge's plugin.json and .claude-plugin/plugin.json both patch-bump to 1.2.5 for the shipped content change (per ADR-0006's version-parity invariant).
  • tests/test-vale-wrap.sh now exercises skill-audit's copy specifically — its fixtures are all SKILL.md-shaped, and only skill-audit's .vale.ini has the matching glob section.
  • The first vX.Y.Z tag is cut once this change and its tests pass, giving external .pre-commit-hooks.yaml consumers something to pin.