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
This commit is contained in:
2026-08-09 10:04:19 +00:00
parent 864e7c689c
commit 1164f3abad
26 changed files with 513 additions and 34 deletions

View File

@@ -61,6 +61,15 @@ repos:
pass_filenames: false
always_run: true
- id: check-vale-style-sync
name: Check Vale style copies are in sync
description: Diff skill-audit's Vale copy against agent-audit's canonical copy
entry: bash scripts/check-vale-style-sync.sh
language: system
stages: [pre-push]
pass_filenames: false
always_run: true
- id: validate-plugins
name: Validate plugins
description: Run claude plugin validate --strict on every plugin directory
@@ -107,13 +116,22 @@ repos:
files: '^plugins/[^/]+/skills/[^/]+/SKILL\.md$'
pass_filenames: true
- id: vale-audit-prefilter
- id: vale-audit-prefilter-skill
stages: ['pre-commit']
name: Vale audit prefilter
description: Run Vale against skill/agent markdown files as a deterministic prefilter for skill-audit/agent-audit
entry: scripts/vale-wrap.sh --config .vale.ini
name: Vale audit prefilter (SKILL.md)
description: Run Vale against SKILL.md files as a deterministic prefilter for skill-audit, via skill-audit's own bundled copy
entry: plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh --config plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini
language: script
files: '^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$'
files: '^plugins/[^/]+/skills/[^/]+/SKILL\.md$'
pass_filenames: true
- id: vale-audit-prefilter-agent
stages: ['pre-commit']
name: Vale audit prefilter (agent files)
description: Run Vale against agent markdown files as a deterministic prefilter for agent-audit, via agent-audit's own bundled copy
entry: plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh --config plugins/kyberforge/skills/agent-audit/assets/vale/.vale.ini
language: script
files: '^plugins/[^/]+/agents/[^/]+\.md$'
pass_filenames: true
- repo: meta

20
.pre-commit-hooks.yaml Normal file
View File

@@ -0,0 +1,20 @@
- id: kyberforge-vale-audit-skill
name: Kyberforge Vale prose audit (SKILL.md)
description: Deterministic prose-pattern prefilter for kyberforge's skill-audit, via its own bundled Vale config/styles
entry: plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh --config plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini
language: script
files: '(^|/)SKILL\.md$'
- id: kyberforge-vale-audit-agent
name: Kyberforge Vale prose audit (agent files)
description: Deterministic prose-pattern prefilter for kyberforge's agent-audit, via its own bundled Vale config/styles
entry: plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh --config plugins/kyberforge/skills/agent-audit/assets/vale/.vale.ini
language: script
files: '(^|/)agents/[^/]+\.md$|\.agent\.md$'
- id: kyberforge-skill-size-check
name: SKILL.md size ceiling
description: Enforce agentskills.io's 500-line/5,000-token SKILL.md size ceiling
entry: scripts/skill-size-check.sh
language: script
files: '(^|/)SKILL\.md$'

View File

@@ -22,7 +22,7 @@ Fall back to raw shell only when no skill covers it.
## Setup and testing
- Install git hooks via `git:pc-run`, wiring all three stages — this repo's `.pre-commit-config.yaml` has no `default_install_hook_types`, so a plain install silently skips `commit-msg` (Conventional Commits) and `pre-push` (tests, manifest check).
- Install the `vale` binary — required by the `vale-audit-prefilter` pre-commit hook, which runs on every commit touching a `SKILL.md` or agent `.md` file. Without it the hook fails 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/. No `vale sync` needed — the `Kyberforge` styles are committed under `styles/`, not downloaded packages.
- Install the `vale` binary — required by the `vale-audit-prefilter-skill`/`-agent` pre-commit hooks, which run on every commit touching a `SKILL.md` or agent `.md` file. Without it 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/. No `vale sync` needed — the `Kyberforge` styles are committed under `plugins/kyberforge/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded packages (see ADR-0014).
- Run `bash tests/run-tests.sh` before considering any change done — it runs every `test-*.sh` script in the repo plus the bats suite (`--bats-only` for just bats). First run auto-initializes the bats submodules; no manual `git submodule update` needed.
- Pushing re-runs the full suite plus `scripts/check-manifests.sh` via the pre-push hook — same commands, so run them locally first.
- Author commits with `git:git-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.

View File

@@ -70,11 +70,11 @@ A companion skill (`core` plugin) that detects a target repo's provider-specific
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 (no `Edit` tool) — it flags findings, it does not rewrite prose. 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 its config lives at the repo root rather than inside `plugins/lint/`: `.vale.ini` plus a custom `Kyberforge` style (`styles/Kyberforge/`) covering description-opener banning ("This skill/agent..."), vague-capability wording ("helps with", "utilize", ...), and generic "see references/ for details" padding — and a `KyberforgeCopilot` style (`styles/KyberforgeCopilot/`) scoped only to `.agent.md` files for the Copilot-only "Use proactively has no effect" check. 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.
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/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/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` pre-commit hook, call `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 once the description is a YAML block scalar (`>`/`|`) spanning 2+ physical lines, which is how most skills/agents in this repo write it. The wrapper flattens the description 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`; single-line descriptions pass through untouched; a relative `--config` path resolves against the caller's cwd, matching bare `vale`, not against the repo root. `tests/test-vale-wrap.sh` regression-tests this. `.vale.ini`'s section globs are path-agnostic (`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]`) and do no scoping: Vale's `*` crosses `/`, so the older `plugins/*/`-prefixed globs already matched `plugins/*/docs/research/examples/**/agents/*.md` and `plugins/*/skills/*/assets/templates/SKILL.md`. Scoping comes from the pre-commit hook's `files:` regex, `^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$` (single-segment, not `.*`, since pre-commit invokes it automatically against whatever staged files match rather than a manually-scoped target), and from the audit skills passing one explicit file per invocation. The path-agnostic globs also close a silent false negative: a skill outside `plugins/` (e.g. project-scope `.claude/skills/foo/SKILL.md`) matched no section, so Vale reported 0 files and exited 0 — which the audits read as clean. Both audits now treat a 0-file Vale run as NOT RUN and fall back to full 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 once the description is a YAML block scalar (`>`/`|`) spanning 2+ physical lines, which is how most skills/agents in this repo write it. The wrapper flattens the description 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`; single-line descriptions pass through untouched; a relative `--config` path resolves against the caller's cwd, matching bare `vale`, not against the repo root. `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 — `^plugins/[^/]+/skills/[^/]+/SKILL\.md$` for `-skill`, `^plugins/[^/]+/agents/[^/]+\.md$` for `-agent` (split into two hooks precisely because one combined hook pointed at only one copy would silently 0-file-skip the other file type) — and from the audit skills passing one explicit file per invocation. A skill outside `plugins/` (e.g. project-scope `.claude/skills/foo/SKILL.md`) matches no glob section, so Vale reports 0 files and exits 0 — which the audits read as clean. 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 41 skill/agent files, 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. Also new is a sibling pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforcing agentskills.io's 500-line/5,000-token `SKILL.md` ceiling — failing only above 500 lines, matching `skill-audit/scripts/validate.sh`'s `<= 500` pass — scoped to `^plugins/[^/]+/skills/[^/]+/SKILL\.md$` only, same as `vale-audit-prefilter`, so it never lints `docs/research/examples/` reference skills. 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.
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 41 skill/agent files, 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. Also new is a sibling pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforcing agentskills.io's 500-line/5,000-token `SKILL.md` ceiling — failing only above 500 lines, matching `skill-audit/scripts/validate.sh`'s `<= 500` pass — scoped to `^plugins/[^/]+/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.

View File

@@ -140,7 +140,9 @@ During write-skill refactor, an "open thread" note (about a deferred research st
## 2026-08-08 — A clean linter result can mean "nothing was checked"
Three separate times in one PR (#85), a check reported success because it had silently not run. (1) Vale's `text.frontmatter.description` scope stops matching once the value is a multi-line YAML block scalar — the style most skills here use — so a repo-wide sweep returned 0 alerts across 49 files and was read as a clean repo. (2) Five of six rules were `level: warning`, but Vale's exit code keys on `error` alone and pre-commit hides output from passing hooks, so those rules were invisible and blocked nothing for two review rounds while the ADR described them as "enforcing immediately." (3) `.vale.ini`'s globs matched no file outside `plugins/`, so Vale printed "0 files" and exited 0, which both audit skills read as "no findings" and used to skip their own judgment passes. Each time the green result was worse than no check at all, because it was cited as positive evidence of cleanliness. Fix: for any new check, prove it fails before trusting that it passes — run it against a deliberately-bad fixture, confirm the failure, then run the real corpus. Where a check can scan zero inputs, assert on the input count, not just the exit code. **Graduation candidate:** three instances of one pattern.
Three separate times in one PR (#85), a check reported success because it had silently not run. (1) Vale's `text.frontmatter.description` scope stops matching once the value is a multi-line YAML block scalar — the style most skills here use — so a repo-wide sweep returned 0 alerts across 49 files and was read as a clean repo. (2) Five of six rules were `level: warning`, but Vale's exit code keys on `error` alone and pre-commit hides output from passing hooks, so those rules were invisible and blocked nothing for two review rounds while the ADR described them as "enforcing immediately." (3) `.vale.ini`'s globs matched no file outside `plugins/`, so Vale printed "0 files" and exited 0, which both audit skills read as "no findings" and used to skip their own judgment passes. Each time the green result was worse than no check at all, because it was cited as positive evidence of cleanliness. Fix: for any new check, prove it fails before trusting that it passes — run it against a deliberately-bad fixture, confirm the failure, then run the real corpus. Where a check can scan zero inputs, assert on the input count, not just the exit code. **Graduated** (4th instance below).
**4th instance (2026-08-09, ADR-0014):** splitting the single root `.vale.ini` into two skill-scoped copies (skill-audit: `SKILL.md` only; agent-audit: agent files only) meant a single retargeted pre-commit hook pointed at agent-audit's copy alone would have silently scanned 0 `SKILL.md` files and exited 0 — caught only because the full corpus was dry-run against both the old and new config and the outputs diffed before the old config was deleted, not because any test asserted on file counts. Standing practice going forward: when a Vale (or any linter) config that serves multiple file-glob scopes is split or moved, dry-run the full corpus through both the old and new config and diff the outputs before removing the superseded source — a hook silently scanning 0 files looks identical to a clean pass.
## 2026-08-08 — One signal, two consumers, no named distinction

View File

@@ -0,0 +1,99 @@
# 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.

View File

@@ -8,5 +8,5 @@
"keywords": [],
"license": "MIT",
"name": "kyberforge",
"version": "1.2.4"
"version": "1.2.5"
}

View File

@@ -13,5 +13,5 @@
"skills": [
"skills/"
],
"version": "1.2.4"
"version": "1.2.5"
}

View File

@@ -36,12 +36,12 @@ metadata:
```bash
bash scripts/validate.sh <path-to-agent-file>
bash scripts/validate-provenance.sh <path-to-agent-file>
"$(git rev-parse --show-toplevel)/scripts/vale-wrap.sh" --config "$(git rev-parse --show-toplevel)/.vale.ini" <path-to-cc-file> <path-to-copilot-file>
scripts/vale-wrap.sh --config assets/vale/.vale.ini <path-to-cc-file> <path-to-copilot-file>
```
The script accepts either the CC file or the Copilot file. It detects provider from extension, derives the counterpart, and runs all structural checks. Note FAILs and SUGGESTIONs for the `### Structure` and `### Provider safety` report dimensions. Findings about missing fields, bad name format, empty body, or missing frontmatter → `### Structure`. Findings about CC-only fields in a Copilot file, Copilot-only fields in a CC file, plugin-silently-ignored fields, body length, or subagent-unavailable tools → `### Provider safety`. A missing counterpart file → `### Pair consistency`.
`vale-wrap.sh` resolves its own path and `.vale.ini` via `git rev-parse --show-toplevel`; pass the absolute paths shown above so the invocation holds from any cwd. Run it against both files of the pair (not just the one passed in). `Kyberforge` applies to both files; `KyberforgeCopilot` applies to the `.agent.md` file only, since its one rule (`Use proactively`) flags CC-specific phrasing that's meaningless in a Copilot description — there's nothing to flag in the CC file, so it isn't scoped there. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one in the `### Description` / `### Body` dimensions citing its rule ID (e.g. `KyberforgeCopilot.ProactivePhrase`). Skip and fall back to Step 2 judgment if vale or `.vale.ini` is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 2 judgment for the dimensions it would have covered.
`vale-wrap.sh` and `.vale.ini` ship inside this skill's own `scripts/`/`assets/` — resolve them relative to this skill's directory the same way `scripts/validate.sh` is resolved above, so the invocation works whether this skill is running from this repo or from an installed plugin cache. Run it against both files of the pair (not just the one passed in). `Kyberforge` applies to both files; `KyberforgeCopilot` applies to the `.agent.md` file only, since its one rule (`Use proactively`) flags CC-specific phrasing that's meaningless in a Copilot description — there's nothing to flag in the CC file, so it isn't scoped there. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one in the `### Description` / `### Body` dimensions citing its rule ID (e.g. `KyberforgeCopilot.ProactivePhrase`). Skip and fall back to Step 2 judgment if vale or `.vale.ini` is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 2 judgment for the dimensions it would have covered.
`validate-provenance.sh` validates the provenance chain between the agent pair's `source_keys` and the plugin-scoped `sources.md` (plugin root — see ADR-0010). It exits 0 silently for non-plugin-scope agents and when no provenance data exists. Note FAILs from this script for the `### Provenance` dimension — surface them verbatim with Why and Fix.

View File

@@ -1,8 +1,5 @@
StylesPath = styles
[**/SKILL.md]
BasedOnStyles = Kyberforge
[**/agents/*.md]
BasedOnStyles = Kyberforge

View File

@@ -34,14 +34,14 @@ metadata:
```bash
bash scripts/validate.sh <skill-dir>
bash scripts/validate-provenance.sh <skill-dir>
"$(git rev-parse --show-toplevel)/scripts/vale-wrap.sh" --config "$(git rev-parse --show-toplevel)/.vale.ini" <skill-dir>/SKILL.md
scripts/vale-wrap.sh --config assets/vale/.vale.ini <skill-dir>/SKILL.md
```
Note any structural FAILs — they will appear in the report as a `### Structure` dimension. If the script cannot execute (python3 unavailable, Bash denied, or permission error), perform structural checks manually: name format, name matches directory, description length ≤1024 chars, SKILL.md ≤500 lines, no unfilled `FILL IN:` placeholders, scripts executable and free of interactive prompts.
Note any Provenance FAILs and INFO findings from `validate-provenance.sh` — they surface in the report as a `### Provenance` dimension (separate from `### Structure`). The script embeds full FAIL/INFO format with Why and Fix per finding; surface them verbatim.
`vale-wrap.sh` resolves its own path and `.vale.ini` via `git rev-parse --show-toplevel`; pass the absolute paths shown above so the invocation holds from any cwd. It applies `.vale.ini`'s `Kyberforge` style — a deterministic prefilter for a subset of the Description/Patterns/Body dimensions below, not a replacement for Step 3. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one citing its rule ID (e.g. `Kyberforge.DescriptionOpener`). Skip and fall back to Step 3 judgment if vale or `.vale.ini` is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 3 judgment for the dimensions it would have covered.
`vale-wrap.sh` and `.vale.ini` ship inside this skill's own `scripts/`/`assets/` — resolve them relative to this skill's directory the same way `scripts/validate.sh` is resolved above, so the invocation works whether this skill is running from this repo or from an installed plugin cache. It applies `.vale.ini`'s `Kyberforge` style — a deterministic prefilter for a subset of the Description/Patterns/Body dimensions below, not a replacement for Step 3. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one citing its rule ID (e.g. `Kyberforge.DescriptionOpener`). Skip and fall back to Step 3 judgment if vale or `.vale.ini` is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 3 judgment for the dimensions it would have covered.
## Step 2 — Read all skill files

View File

@@ -0,0 +1,4 @@
StylesPath = styles
[**/SKILL.md]
BasedOnStyles = Kyberforge

View File

@@ -0,0 +1,7 @@
extends: existence
message: "Description opens with '%s' — use an imperative 'Use when...' opener instead"
level: error
scope: text.frontmatter.description
ignorecase: true
raw:
- '^This (skill|agent)\b'

View File

@@ -0,0 +1,7 @@
extends: existence
message: "Generic reference pointer: '%s' — use the specific 'If X, read `references/file.md`' form instead"
level: error
scope: text
ignorecase: true
raw:
- 'see references?/? for (more )?(info|information|details)\b'

View File

@@ -0,0 +1,7 @@
extends: existence
message: "Don't start a sentence with '%s' — name the subject directly"
level: error
scope: sentence
ignorecase: false
raw:
- '^There\s(is|are)\b'

View File

@@ -0,0 +1,10 @@
extends: existence
message: "Vague capability wording: '%s' — state the capability precisely instead"
level: error
scope: text.frontmatter.description
ignorecase: true
tokens:
- helps with
- utilize
- assists with
- used for

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
set -euo pipefail
# Works around a Vale limitation: the `text.frontmatter.description` NLP scope
# silently stops matching once the `description:` value is a YAML block scalar
# (`>`/`|`) spanning 2+ physical lines — the style used by most skills/agents in
# this repo. Flattens the description to one physical line in a scratch copy
# (padding with blank lines so every other line number is unchanged), then runs
# the real `vale` binary against the copies. Drop-in replacement for calling
# `vale` directly: same args, same exit code.
#
# "Same args" means relative paths — `--config` values and file arguments alike
# — resolve against the caller's current directory, exactly as bare `vale`
# resolves them. (An earlier version resolved them against the repo root, an
# invented convention that hard-errored on `--config ../../.vale.ini` from a
# subdirectory and, worse, silently dropped file arguments that didn't happen to
# resolve from the repo root — skipping the flattening this script exists for.)
#
# Vale prints each file path exactly as it was handed to it, so the scratch tree
# mirrors the caller's absolute cwd: a relative file argument is passed through
# verbatim and resolves to its flattened copy, keeping the report byte-identical
# to bare `vale`'s. An absolute file argument inside the cwd is relativized to
# keep that property. Only an absolute path outside the cwd is rewritten to its
# scratch copy and so reports a scratch path — unavoidable, since a file can
# only be read from where it actually is.
cwd="$(pwd -P)"
vale_args=()
file_args=()
config_next=false
for arg in "$@"; do
if [[ "$config_next" == true ]]; then
config_next=false
if [[ "$arg" == /* ]]; then
vale_args+=("$arg")
else
vale_args+=("$cwd/$arg")
fi
continue
fi
case "$arg" in
--config)
vale_args+=("$arg")
config_next=true
continue
;;
--config=/*)
vale_args+=("$arg")
continue
;;
--config=*)
vale_args+=("--config=$cwd/${arg#--config=}")
continue
;;
esac
# `-f` resolves relative paths against the caller's cwd, same as vale does.
if [[ "$arg" != -* && -f "$arg" ]]; then
# An absolute path inside the caller's cwd is relativized so the report cites
# a path that resolves against the real tree. Left absolute, it would be
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
# path to a file that is deleted on exit, which reads as a bug in any report
# quoting it. Absolute paths outside the cwd have no relative form and keep
# the scratch-path behaviour documented above.
if [[ "$arg" == "$cwd"/* ]]; then
file_args+=("${arg#"$cwd"/}")
else
file_args+=("$arg")
fi
else
vale_args+=("$arg")
fi
done
if [[ ${#file_args[@]} -eq 0 ]]; then
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
# block waiting on a pipe that will never carry content.
exec vale "${vale_args[@]}" < /dev/null
fi
tmpdir="$(realpath -m "$(mktemp -d)")"
trap 'rm -rf "$tmpdir"' EXIT
# Mirror of the caller's cwd inside the scratch tree; relative file arguments
# are resolved from here.
mirror="$tmpdir$cwd"
mkdir -p "$mirror"
argv_files=()
for arg in "${file_args[@]}"; do
if [[ "$arg" == /* ]]; then
dest="$tmpdir$arg"
else
dest="$mirror/$arg"
fi
dest="$(realpath -m "$dest")"
# A file argument with enough leading `..` to climb past the mirror root would
# write outside the scratch dir. The real filesystem clamps such a path at
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
case "$dest" in
"$tmpdir"/*) ;;
*)
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
exit 2
;;
esac
mkdir -p "$(dirname "$dest")"
python3 - "$arg" "$dest" <<'PYTHON'
import re
import sys
src, dest = sys.argv[1], sys.argv[2]
with open(src) as fh:
content = fh.read()
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
if fm_match:
fm = fm_match.group(2)
# Only `>`/`>-`/`>+` (folded) scalars break Vale's frontmatter-description
# scope. `|`/`|-`/`|+` (literal) scalars already work fine with bare vale,
# so they're deliberately left unmatched here.
header_m = re.search(r'^description:[ \t]*(>[+-]?)[ \t]*\n', fm, re.MULTILINE)
if header_m:
# Body capture is indentation-based and blank-line-tolerant, per YAML
# block-scalar rules: a blank line (any amount of whitespace) always
# stays inside the block; the indent is set by the first content line;
# the block ends at the first line indented less than that, or EOF.
rest = fm[header_m.end():]
indent = None
body_lines = []
for line in rest.splitlines(keepends=True):
text = line.rstrip('\n')
if text.strip() == '':
body_lines.append(line)
continue
line_indent = len(text) - len(text.lstrip(' \t'))
if indent is None:
indent = line_indent
elif line_indent < indent:
break
body_lines.append(line)
raw = ''.join(body_lines)
if raw.count('\n') >= 2:
flat = re.sub(r'\s+', ' ', raw).strip()
# YAML single-quoted scalars have no backslash-escape mechanism at
# all, so wrapping in single quotes sidesteps the backslash-escape
# bug entirely for embedded double quotes, backslashes, and
# non-ASCII text. The one YAML-spec-correct way to embed a literal
# apostrophe is to double it ('') — but Vale's own frontmatter
# scanner isn't a full YAML parser and doesn't understand that
# doubling: empirically, it silently truncates the value at the
# first ' it sees, hiding everything after it from the NLP scope
# (a different flavor of the same bug this whole script exists to
# work around). Since this copy is scratch-only and never written
# back, sidestep it by substituting a Unicode right single
# quotation mark (U+2019) for any literal apostrophe instead of
# doubling it — visually a smart quote, but never triggers a YAML
# escape sequence at all.
flat_q = "'" + flat.replace("'", "’") + "'"
pad = '\n' * raw.count('\n')
start = header_m.start()
end = header_m.end() + len(raw)
new_fm = fm[:start] + f'description: {flat_q}\n{pad}' + fm[end:]
content = fm_match.group(1) + new_fm + fm_match.group(3) + content[fm_match.end():]
with open(dest, 'w') as fh:
fh.write(content)
PYTHON
if [[ "$arg" == /* ]]; then
argv_files+=("$dest")
else
argv_files+=("$arg")
fi
done
cd "$mirror"
vale "${vale_args[@]}" "${argv_files[@]}"

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
# Kyberforge's Vale prefilter is duplicated into skill-audit and agent-audit's own
# scripts/assets (per plugins/kyberforge/skills/skill-author/references/deployment-modes.md's
# no-cross-skill-path rule: a plugin's cache-install copy only includes each skill's own files).
# agent-audit's copy is canonical — it's the superset (Kyberforge + KyberforgeCopilot) that the
# repo root's own pre-commit hook and .pre-commit-hooks.yaml both consume. This fails the build
# if skill-audit's copy has drifted from it, since nothing else would catch a rule fix landing in
# only one of the two. Run from repo root or pass REPO_ROOT as arg.
REPO_ROOT="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
FAIL=0
err() { echo " FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
SKILL_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/skill-audit"
AGENT_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/agent-audit"
if [[ ! -d "$SKILL_AUDIT" || ! -d "$AGENT_AUDIT" ]]; then
exit 0
fi
if ! diff -q "$SKILL_AUDIT/scripts/vale-wrap.sh" "$AGENT_AUDIT/scripts/vale-wrap.sh" >/dev/null 2>&1; then
err "scripts/vale-wrap.sh differs between skill-audit and agent-audit"
fi
if ! diff -rq "$SKILL_AUDIT/assets/vale/styles/Kyberforge" "$AGENT_AUDIT/assets/vale/styles/Kyberforge" >/dev/null 2>&1; then
err "assets/vale/styles/Kyberforge differs between skill-audit and agent-audit"
fi
if [[ $FAIL -gt 0 ]]; then
echo "Vale style sync check failed: $FAIL error(s). agent-audit's copy is canonical — sync skill-audit's copy to match." >&2
exit 1
fi

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT="$REPO_ROOT/scripts/check-vale-style-sync.sh"
PASS=0
FAIL=0
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# Helper: make a fixture repo with skill-audit/agent-audit's Vale copies, in sync by default.
make_fixture() {
local dir
dir="$(mktemp -d)"
local skill_audit="$dir/plugins/kyberforge/skills/skill-audit"
local agent_audit="$dir/plugins/kyberforge/skills/agent-audit"
mkdir -p "$skill_audit/scripts" "$skill_audit/assets/vale/styles/Kyberforge"
mkdir -p "$agent_audit/scripts" "$agent_audit/assets/vale/styles/Kyberforge"
echo '#!/usr/bin/env bash' > "$skill_audit/scripts/vale-wrap.sh"
echo 'echo wrap' >> "$skill_audit/scripts/vale-wrap.sh"
cp "$skill_audit/scripts/vale-wrap.sh" "$agent_audit/scripts/vale-wrap.sh"
echo 'extends: existence' > "$skill_audit/assets/vale/styles/Kyberforge/Rule.yml"
cp "$skill_audit/assets/vale/styles/Kyberforge/Rule.yml" "$agent_audit/assets/vale/styles/Kyberforge/Rule.yml"
echo "$dir"
}
# --- 1. Exits 0 when the two copies are in sync ---
echo ""
echo "--- exits 0 when skill-audit and agent-audit copies are in sync ---"
FIXTURE="$(make_fixture)"
trap 'rm -rf "$FIXTURE"' EXIT
if bash "$SCRIPT" "$FIXTURE" > /dev/null 2>&1; then
pass "exits 0 when copies are in sync"
else
fail "exited non-zero against in-sync copies"
fi
# --- 2. Exits 1 when vale-wrap.sh differs between the two copies ---
echo ""
echo "--- exits 1 when vale-wrap.sh differs ---"
FIXTURE2="$(make_fixture)"
trap 'rm -rf "$FIXTURE" "$FIXTURE2"' EXIT
echo 'echo different' >> "$FIXTURE2/plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh"
if bash "$SCRIPT" "$FIXTURE2" > /dev/null 2>&1; then
fail "exited 0 when vale-wrap.sh copies differ — expected exit 1"
else
pass "exits non-zero when vale-wrap.sh copies differ"
fi
# --- 3. Exits 1 when a style rule differs between the two copies ---
echo ""
echo "--- exits 1 when a Kyberforge style rule differs ---"
FIXTURE3="$(make_fixture)"
trap 'rm -rf "$FIXTURE" "$FIXTURE2" "$FIXTURE3"' EXIT
echo 'level: error' >> "$FIXTURE3/plugins/kyberforge/skills/agent-audit/assets/vale/styles/Kyberforge/Rule.yml"
if bash "$SCRIPT" "$FIXTURE3" > /dev/null 2>&1; then
fail "exited 0 when a style rule differs — expected exit 1"
else
pass "exits non-zero when a Kyberforge style rule differs between copies"
fi
# --- 4. Exits 1 when a rule file exists in only one copy ---
echo ""
echo "--- exits 1 when a rule file is missing from one copy ---"
FIXTURE4="$(make_fixture)"
trap 'rm -rf "$FIXTURE" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT
echo 'extends: existence' > "$FIXTURE4/plugins/kyberforge/skills/agent-audit/assets/vale/styles/Kyberforge/Extra.yml"
if bash "$SCRIPT" "$FIXTURE4" > /dev/null 2>&1; then
fail "exited 0 when a rule file exists in only one copy — expected exit 1"
else
pass "exits non-zero when a rule file is missing from one copy"
fi
# --- 5. Exits 0 (no-op) when kyberforge isn't present in the target repo ---
echo ""
echo "--- exits 0 when kyberforge skills are absent (no-op) ---"
FIXTURE5="$(mktemp -d)"
trap 'rm -rf "$FIXTURE" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT
if bash "$SCRIPT" "$FIXTURE5" > /dev/null 2>&1; then
pass "exits 0 as a no-op when skill-audit/agent-audit don't exist"
else
fail "exited non-zero when skill-audit/agent-audit are simply absent"
fi
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]

View File

@@ -6,7 +6,12 @@
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT="$REPO_ROOT/scripts/vale-wrap.sh"
# skill-audit's copy is used here (not agent-audit's) because every fixture below is a
# SKILL.md — only skill-audit's .vale.ini has the [**/SKILL.md] glob section. vale-wrap.sh
# itself is an identical copy in both skills, so which one SCRIPT points at doesn't matter.
SKILL_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/skill-audit"
SCRIPT="$SKILL_AUDIT/scripts/vale-wrap.sh"
VALE_CONFIG="$SKILL_AUDIT/assets/vale/.vale.ini"
PASS=0
FAIL=0
@@ -55,7 +60,7 @@ echo ""
echo "--- catches vague wording in a single-line description ---"
FIXTURE1="$(make_fixture 1)"
trap 'rm -rf "$FIXTURE1"' EXIT
if run_wrap "$FIXTURE1" --config "$REPO_ROOT/.vale.ini" \
if run_wrap "$FIXTURE1" --config "$VALE_CONFIG" \
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when description is a single physical line"
else
@@ -67,7 +72,7 @@ echo ""
echo "--- catches vague wording in a multi-line folded description ---"
FIXTURE2="$(make_fixture 2)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2"' EXIT
if run_wrap "$FIXTURE2" --config "$REPO_ROOT/.vale.ini" \
if run_wrap "$FIXTURE2" --config "$VALE_CONFIG" \
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when description spans 2+ physical lines"
else
@@ -80,7 +85,7 @@ echo "--- preserves total line count when flattening ---"
FIXTURE3="$(make_fixture 3)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3"' EXIT
ORIG_LINES=$(wc -l < "$FIXTURE3/plugins/testplugin/skills/zzzskill/SKILL.md")
OUT=$(run_wrap "$FIXTURE3" --config "$REPO_ROOT/.vale.ini" \
OUT=$(run_wrap "$FIXTURE3" --config "$VALE_CONFIG" \
plugins/testplugin/skills/zzzskill/SKILL.md)
MAX_LINE=$(echo "$OUT" | grep -oE '^[[:space:]]*[0-9]+:[0-9]+' | tr -d '[:space:]' | cut -d: -f1 | sort -n | tail -1)
if [[ -n "$MAX_LINE" ]] && (( MAX_LINE <= ORIG_LINES )); then
@@ -118,7 +123,7 @@ Body.
EOF
)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT
if run_wrap "$FIXTURE4" --config "$REPO_ROOT/.vale.ini" \
if run_wrap "$FIXTURE4" --config "$VALE_CONFIG" \
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when the description contains a double quote"
else
@@ -140,7 +145,7 @@ Body.
EOF
)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT
OUT5=$(run_wrap "$FIXTURE5" --config "$REPO_ROOT/.vale.ini" \
OUT5=$(run_wrap "$FIXTURE5" --config "$VALE_CONFIG" \
plugins/testplugin/skills/zzzskill/SKILL.md)
if echo "$OUT5" | grep -q "VagueWording"; then
pass "flags vague wording when the description contains an apostrophe"
@@ -168,7 +173,7 @@ Body.
EOF
)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6"' EXIT
if run_wrap "$FIXTURE6" --config "$REPO_ROOT/.vale.ini" \
if run_wrap "$FIXTURE6" --config "$VALE_CONFIG" \
plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when the description has a backslash and non-ASCII text"
else
@@ -191,7 +196,7 @@ Body.
EOF
)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7"' EXIT
OUT7=$(run_wrap "$FIXTURE7" --config "$REPO_ROOT/.vale.ini" \
OUT7=$(run_wrap "$FIXTURE7" --config "$VALE_CONFIG" \
plugins/testplugin/skills/zzzskill/SKILL.md)
if echo "$OUT7" | grep -q "Traceback"; then
fail "crashed while flattening a description with a blank line between paragraphs"
@@ -213,8 +218,8 @@ echo ""
echo "--- resolves a cwd-relative --config from a subdirectory (equals and two-argv forms) ---"
FIXTURE8="$(mktemp -d)"
(cd "$FIXTURE8" && git init -q)
cp "$REPO_ROOT/.vale.ini" "$FIXTURE8/.vale.ini"
cp -r "$REPO_ROOT/styles" "$FIXTURE8/styles"
cp "$VALE_CONFIG" "$FIXTURE8/.vale.ini"
cp -r "$SKILL_AUDIT/assets/vale/styles" "$FIXTURE8/styles"
mkdir -p "$FIXTURE8/plugins/testplugin/skills/zzzskill"
{
echo "---"
@@ -280,7 +285,7 @@ fi
# --- 9. Zero file args (or a file list that filters to nothing) exits promptly ---
echo ""
echo "--- exits promptly instead of hanging on stdin when no files are passed ---"
if timeout 5 bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" < <(sleep 100) >/dev/null 2>&1; then
if timeout 5 bash "$SCRIPT" --config "$VALE_CONFIG" < <(sleep 100) >/dev/null 2>&1; then
pass "exits promptly with zero file args"
else
RC=$?
@@ -293,7 +298,7 @@ fi
echo ""
echo "--- exits promptly when a file list filters down to nothing ---"
if timeout 5 bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" --no-such-flag < <(sleep 100) >/dev/null 2>&1; then
if timeout 5 bash "$SCRIPT" --config "$VALE_CONFIG" --no-such-flag < <(sleep 100) >/dev/null 2>&1; then
pass "exits promptly when no file-shaped args remain"
else
RC=$?
@@ -310,7 +315,7 @@ echo "--- lints an absolute path to a skill file instead of silently skipping it
FIXTURE10="$(make_fixture 2)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10"' EXIT
ABS_FILE10="$FIXTURE10/plugins/testplugin/skills/zzzskill/SKILL.md"
if run_wrap "$FIXTURE10" --config "$REPO_ROOT/.vale.ini" "$ABS_FILE10" | grep -q "VagueWording"; then
if run_wrap "$FIXTURE10" --config "$VALE_CONFIG" "$ABS_FILE10" | grep -q "VagueWording"; then
pass "an absolute path is linted, not silently skipped"
else
fail "an absolute path was silently skipped — the bug this test guards against"
@@ -332,8 +337,8 @@ EOF
)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11"' EXIT
REL11="plugins/testplugin/skills/zzzskill/SKILL.md"
WRAPPED_OUT=$(run_wrap "$FIXTURE11" --config "$REPO_ROOT/.vale.ini" "$REL11")
BARE_OUT=$(cd "$FIXTURE11" && vale --config "$REPO_ROOT/.vale.ini" "$REL11" 2>&1 || true)
WRAPPED_OUT=$(run_wrap "$FIXTURE11" --config "$VALE_CONFIG" "$REL11")
BARE_OUT=$(cd "$FIXTURE11" && vale --config "$VALE_CONFIG" "$REL11" 2>&1 || true)
if [[ "$WRAPPED_OUT" == "$BARE_OUT" ]]; then
pass "literal (|) block scalar output matches bare vale exactly — untouched by flattening"
else