.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
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.share deleted. Two copies remain:plugins/kyberforge/skills/agent-audit/assets/vale/(canonical, superset) andplugins/kyberforge/skills/skill-audit/assets/vale/(subset,Kyberforgeonly). plugins/kyberforge'splugin.jsonand.claude-plugin/plugin.jsonboth patch-bump to1.2.5for the shipped content change (per ADR-0006's version-parity invariant).tests/test-vale-wrap.shnow exercises skill-audit's copy specifically — its fixtures are allSKILL.md-shaped, and only skill-audit's.vale.inihas the matching glob section.- The first
vX.Y.Ztag is cut once this change and its tests pass, giving external.pre-commit-hooks.yamlconsumers something to pin. - Cutting the tag is not left to memory.
scripts/check-release-needed.sh, wired atpre-push, hard-fails — but only whenPRE_COMMIT_REMOTE_BRANCH(set by pre-commit'shook-implfor pre-push hooks) isrefs/heads/main— if any path.pre-commit-hooks.yamlexposes changed since the last tag reachable fromHEAD. 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 problemrepo: local(above) already avoids for this repo's own dev-time gate. A tag not existing at all is also a hard fail onmain, covering the very first release. This is deterministic tooling, not a standing instruction to remember — consistent withcheck-manifests.sh/check-vale-style-sync.shalready using the same pre-push, main-agnostic-elsewhere pattern.