Files
holocron/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md
Defame1297 4d018af03c fix(lint): hard-fail on main when a release tag is needed
.pre-commit-hooks.yaml now exposes hooks to external consumers pinning
rev: <tag>, but nothing enforced that a tag actually gets cut when the
files it references change — relying on memory is exactly what this
repo's governance rules say to avoid for a repeatable, deterministic
check.

scripts/check-release-needed.sh hard-fails at pre-push, but only when
PRE_COMMIT_REMOTE_BRANCH (set by pre-commit's hook-impl) is
refs/heads/main: it diffs .pre-commit-hooks.yaml's referenced paths
against the last tag reachable from HEAD, and fails if either no tag
exists yet or something changed since. It's a silent no-op on every
other branch — hard-failing on feature-branch pushes mid-review would
force a premature tag on a commit that might not survive a
squash-merge, the exact risk the repo: local (vs. pinned self-
reference) decision in ADR-0014 already avoids for this repo's own
dev-time gate.

Verified against the real git pre-push hook path (not just the script
in isolation): simulated stdin matching git's pre-push protocol through
.git/hooks/pre-push, confirmed it correctly fires and fails when
targeting main with no tag, and is silent otherwise.

ADR: 0014
Refs: #87
2026-08-09 10:20:43 +00:00

7.8 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.
  • Cutting the tag is not left to memory. scripts/check-release-needed.sh, wired at pre-push, hard-fails — but only when PRE_COMMIT_REMOTE_BRANCH (set by pre-commit's hook-impl for pre-push hooks) is refs/heads/main — if any path .pre-commit-hooks.yaml exposes changed since the last tag reachable from HEAD. It is a silent no-op on every other branch: hard-failing on feature-branch pushes mid-review would force a premature tag on a commit that might not survive a squash-merge, the exact problem repo: local (above) already avoids for this repo's own dev-time gate. A tag not existing at all is also a hard fail on main, covering the very first release. This is deterministic tooling, not a standing instruction to remember — consistent with check-manifests.sh/check-vale-style-sync.sh already using the same pre-push, main-agnostic-elsewhere pattern.