fix(lint): make the Vale gate actually gate, drop VagueQualifier

Round-3 review of PR #85 found the "enforcing" pre-commit hook enforced
nothing. Vale's exit code keys on error-level alerts alone: five of the
six rules were level: warning, so they exited 0, and pre-commit hides
output from a passing hook — the alerts were invisible and blocked
nothing. ADR-0013 rejected a report-only trial tier and then shipped one
by accident.

Flatten every rule to level: error. Vale's own exit code is then correct,
so the hook entry drops to a bare vale-wrap.sh call and the graded
error->FAIL / warning->SUGGESTION mapping disappears from both audit
skills: every alert is a FAIL, in the gate and the audit alike. No
ignorable tier, matching shellcheck, the test suite and
conventional-pre-commit.

Delete Kyberforge.VagueQualifier. Measured against the 41 skill/agent
files as they stood before the rule ever ran: 2 hits. One marginal
("very different" -> "fundamentally different"), one an unfixable false
positive — caveman/SKILL.md quotes "of course" as an example of filler,
a mention not a use — which forced the only Vale suppression comments in
the repo. Those four lines go with it; two of them were dead anyway,
suppressing a frontmatter-scoped rule on a body line. Held-out prose (273
files) fired 15 times, 9 inside out-of-scope research examples and the
rest one word in two idioms in a single doc. SentenceOpenerThereIs
survives: 22 held-out hits, both in-corpus hits clean rewrites, zero
suppressions.

Widen .vale.ini's globs to [**/SKILL.md], [**/agents/*.md] and
[**/*.agent.md]. The plugins/*/-prefixed globs scoped nothing — Vale's *
crosses /, so they already matched docs/research/examples/**/agents/*.md
and assets/templates/SKILL.md, the two paths CONTEXT.md claimed they
excluded. Scoping is and was the hook's files: regex. The old globs also
hid a silent false negative: a skill outside plugins/ matched no section,
so Vale reported 0 files and exited 0, which both audits read as clean.
They now treat a 0-file run as NOT RUN and fall back to full judgment.

Also:
- vale-wrap.sh resolves relative --config values and file arguments
  against the caller's cwd, as vale does, instead of the repo root, which
  hard-errored from a subdirectory and silently skipped flattening for
  file args that did not resolve from the root. Absolute paths inside the
  cwd are relativized so reports cite resolvable paths, not scratch ones.
- vale-run's exit-code model was documented backwards ("exits non-zero
  whenever it finds an alert at or above MinAlertLevel") and would have
  led anyone following it to build a gate that passes everything. Its
  Markdown suppression syntax was MDX-only and does not suppress in .md;
  corrected in the skill and its troubleshooting reference, with
  backtick/fence exemption documented as the first resort.
- skill-size-check.sh fails only above 500 lines, agreeing with
  skill-audit's validate.sh <= 500 pass.
- ADR-0013 and CONTEXT.md amended to match, recording why graded
  severities cannot gate.

Verified: 9 test scripts / 15 vale-wrap cases pass; vale-audit-prefilter,
skill-size-check and shellcheck pass --all-files; check-manifests and
claude plugin validate --strict clean. New tests fail against the old
script (3 of them) and pass against the new one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
This commit is contained in:
2026-08-08 20:42:15 +00:00
parent 210b192613
commit 149d564f6a
18 changed files with 269 additions and 136 deletions

View File

@@ -111,7 +111,7 @@ repos:
stages: ['pre-commit'] stages: ['pre-commit']
name: Vale audit prefilter name: Vale audit prefilter
description: Run Vale against skill/agent markdown files as a deterministic prefilter for skill-audit/agent-audit 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 --minAlertLevel=warning entry: scripts/vale-wrap.sh --config .vale.ini
language: script language: script
files: '^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$' files: '^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$'
pass_filenames: true pass_filenames: true

View File

@@ -1,11 +1,10 @@
StylesPath = styles StylesPath = styles
MinAlertLevel = suggestion
[plugins/*/skills/*/SKILL.md] [**/SKILL.md]
BasedOnStyles = Kyberforge BasedOnStyles = Kyberforge
[plugins/*/agents/*.md] [**/agents/*.md]
BasedOnStyles = Kyberforge BasedOnStyles = Kyberforge
[plugins/*/agents/*.agent.md] [**/*.agent.md]
BasedOnStyles = Kyberforge, KyberforgeCopilot BasedOnStyles = Kyberforge, KyberforgeCopilot

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. 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) ### 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. `error` alerts map to FAIL, `warning`/`suggestion` map to SUGGESTION. 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, two body-wide prose-pattern checks (vague-qualifier filler, "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 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.
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. `tests/test-vale-wrap.sh` regression-tests this. Both call sites still scope every invocation to the specific file(s) being audited, never a repo-wide sweep — Vale's glob matching crosses directory boundaries (`plugins/*/agents/*.md` matches nested `docs/research/examples/**/agents/*.md` too), so scoping is what keeps research-example files out of the audit's lint pass. The pre-commit hook's own glob is tightened to `^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$` (single-segment, not `.*`) for the same reason, since pre-commit invokes it automatically against whatever staged files match rather than a manually-scoped target. 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.
This scope expands per ADR-0013: cherry-picked low-noise `write-good`/`alex` rules landed in `styles/Kyberforge` as two new rule files, `Kyberforge.VagueQualifier` and `Kyberforge.SentenceOpenerThereIs`, plus a new sibling pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforcing agentskills.io's 500-line/5,000-token `SKILL.md` ceiling — 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). 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.
### LESSONS.md ### 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. 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

@@ -9,8 +9,8 @@ deferred PR #85 review item to broaden that coverage, retroactively captures #84
(since it was never recorded as a decision in its own right), and layers the expansion on top (since it was never recorded as a decision in its own right), and layers the expansion on top
without reversing or weakening the original four rules. without reversing or weakening the original four rules.
**File scope stays the same.** `SKILL.md` plus agent files (`plugins/*/agents/*.md`, **File scope stays the same.** `SKILL.md` plus agent files (`**/agents/*.md`,
`plugins/*/agents/*.agent.md`) only — matching the existing prefilter's globs. Skill-level `**/*.agent.md`) only — matching the existing prefilter's globs. Skill-level
`README.md` files and `plugin.json` manifests are not added: README.md files are navigational, not `README.md` files and `plugin.json` manifests are not added: README.md files are navigational, not
spec-governed content, and `plugin.json` is JSON, not prose Vale can meaningfully lint. spec-governed content, and `plugin.json` is JSON, not prose Vale can meaningfully lint.
@@ -51,7 +51,13 @@ length ceiling, not a text pattern, so it isn't a Vale rule — it becomes a new
and pre-commit hook, sibling to the existing `skill-frontmatter` hook. and pre-commit hook, sibling to the existing `skill-frontmatter` hook.
**Rules land directly in `styles/Kyberforge`, enforcing immediately.** No trial/report-only tier **Rules land directly in `styles/Kyberforge`, enforcing immediately.** No trial/report-only tier
is introduced (see Considered Options). The implementation pass finalizes the cherry-picked is introduced (see Considered Options). "Enforcing immediately" holds only because every rule in
both styles is `level: error`: Vale's exit code keys on `error`-level alerts alone, so a
`warning`- or `suggestion`-level rule prints an alert and still exits 0, and pre-commit suppresses
output from hooks that pass — such a rule is invisible and blocks nothing. Every Vale alert is
therefore a FAIL, in the audit skills and in the blocking pre-commit hook alike, with no ignorable
tier; that matches every other gate in this repo (shellcheck, the test suite,
conventional-pre-commit). The implementation pass finalizes the cherry-picked
`write-good`/`alex` rules and any new spec-derived rule wording, runs the full set against the `write-good`/`alex` rules and any new spec-derived rule wording, runs the full set against the
existing SKILL.md/agent-file corpus, fixes any resulting violations across that corpus, and lands existing SKILL.md/agent-file corpus, fixes any resulting violations across that corpus, and lands
the rule changes and the corpus fixes as one atomic commit — the same enforcement model as the the rule changes and the corpus fixes as one atomic commit — the same enforcement model as the
@@ -68,14 +74,29 @@ under that directory automatically — there's no partial/opt-in application wit
rule dropped straight into `styles/Kyberforge` goes live in the blocking pre-commit hook rule dropped straight into `styles/Kyberforge` goes live in the blocking pre-commit hook
immediately. Rejected in favor of finalizing rules directly and fixing violations via subagent immediately. Rejected in favor of finalizing rules directly and fixing violations via subagent
before committing: simpler, no new trial-config machinery to build or maintain — at the cost of no before committing: simpler, no new trial-config machinery to build or maintain — at the cost of no
standing report-only tier for future candidate rules. standing report-only tier for future candidate rules. Note that the first implementation shipped
graded severities (`error`/`warning`/`suggestion`) and thereby recreated the rejected option by
accident: the five non-`error` rules never affected an exit code and never surfaced output through
a passing pre-commit hook, so they were a report-only tier that reported to nobody. Flattening
every rule to `level: error` is what actually implements this decision.
## Consequences ## Consequences
- `styles/Kyberforge/` gained two new rule files, cherry-picked from `write-good`/`alex` as - `styles/Kyberforge/` gained one new rule file, cherry-picked from `write-good`/`alex` as
low-noise against this repo's corpus: `VagueQualifier.yml` and `SentenceOpenerThereIs.yml`. low-noise against this repo's corpus: `SentenceOpenerThereIs.yml` (22 hits across 273 held-out
markdown files; both in-corpus hits were clean rewrites, needing no suppression).
- A second candidate, `VagueQualifier.yml`, was cherry-picked and then dropped. Against the 41
skill/agent files it hit twice: one marginal real finding (`prototype/SKILL.md`, "very different"
→ "fundamentally different") and one false positive (`caveman/SKILL.md`, which *quotes* `of
course` as an example of filler — a mention, not a use) that no rewrite could clear, forcing the
repo's only Vale suppression comments. Of its 15 held-out hits, 9 were in `docs/research/examples/`
(out-of-scope upstream material) and the remaining 6 were the word "very" in two idioms in a
single research doc, each already adjacent to the hard number carrying the fact. One marginal
catch does not pay for a permanent suppression, so the rule is deleted and this ADR's
"cherry-picked rules" is one rule, not two.
- A new pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforces the - A new pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforces the
500-line/5,000-token `SKILL.md` ceiling, sibling to `skill-frontmatter`. 500-line/5,000-token `SKILL.md` ceiling, sibling to `skill-frontmatter`. It fails only *above*
500 lines, matching `skill-audit/scripts/validate.sh`'s long-standing `<= 500` pass.
- `styles/KyberforgeTrial/` and `.vale.trial.ini` were deliberately not created — noted here so a - `styles/KyberforgeTrial/` and `.vale.trial.ini` were deliberately not created — noted here so a
future reader doesn't wonder if a trial tier was forgotten. future reader doesn't wonder if a trial tier was forgotten.
- The styles-portability question — whether `styles/` and `.vale.ini` should move into - The styles-portability question — whether `styles/` and `.vale.ini` should move into
@@ -87,7 +108,8 @@ standing report-only tier for future candidate rules.
not silently forgotten. not silently forgotten.
**What this ADR's implementation pass did:** synced and trialed `write-good`/`alex` against the **What this ADR's implementation pass did:** synced and trialed `write-good`/`alex` against the
existing SKILL.md/agent-file corpus, cherry-picked the two low-noise rules above into existing SKILL.md/agent-file corpus, cherry-picked the one low-noise rule above into
`styles/Kyberforge`, wrote `scripts/skill-size-check.sh` and its pre-commit hook, fixed the `styles/Kyberforge`, wrote `scripts/skill-size-check.sh` and its pre-commit hook, fixed the
resulting corpus violations, and landed the rule changes and corpus fixes as one atomic commit — resulting corpus violations, and landed the rule changes and corpus fixes as one atomic commit —
matching the enforcement model described above (no partial or opt-in state). matching the enforcement model described above (no partial or opt-in state), with every rule at
`level: error` so that model is real rather than nominal.

View File

@@ -15,12 +15,7 @@ ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drif
## Rules ## Rules
<!-- vale Kyberforge.VagueQualifier = NO --> Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough.
<!-- vale Kyberforge.VagueWording = NO -->
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging.
<!-- vale Kyberforge.VagueQualifier = YES -->
<!-- vale Kyberforge.VagueWording = YES -->
Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough.
Technical terms stay exact. Code blocks unchanged. Errors quoted exact. Technical terms stay exact. Code blocks unchanged. Errors quoted exact.

View File

@@ -41,7 +41,7 @@ bash scripts/validate-provenance.sh <path-to-agent-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`. 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`, so it runs correctly regardless of the caller's 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. Map `error` → `FAIL` and `warning`/`suggestion` → `SUGGESTION` in the `### Description` / `### Body` dimensions, citing the rule ID (e.g. `KyberforgeCopilot.ProactivePhrase`). Skip and fall back to Step 2 judgment if vale or `.vale.ini` is unavailable. `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.
`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. `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.
@@ -53,7 +53,7 @@ Read both agent files. Work through each dimension internally. Collect findings
**Description (both files):** **Description (both files):**
- Action-verb opening: description starts with a verb ("Reviews...", "Analyzes...", "Generates...") — FAIL if absent. Vale's `Kyberforge.DescriptionOpener` alert flags the specific known-bad "This agent..." opener directly; verifying an arbitrary opening word is genuinely a strong verb still requires judgment. - Action-verb opening: description starts with a verb ("Reviews...", "Analyzes...", "Generates...") — FAIL if absent. Vale's `Kyberforge.DescriptionOpener` alert flags the specific known-bad "This agent..." opener directly; verifying an arbitrary opening word is genuinely a strong verb still requires judgment.
- Specificity: is the trigger condition stated precisely? — SUGGESTION if vague. Vale's `Kyberforge.VagueWording` alert covers known filler ("helps with", "utilize", ...) directly; report those without re-deriving by judgment. - Specificity: is the trigger condition stated precisely? — SUGGESTION if vague. Vale's `Kyberforge.VagueWording` alert covers known filler ("helps with", "utilize", ...) directly; report those as FAILs without re-deriving by judgment.
- `Use proactively` in a Copilot description: Vale's `KyberforgeCopilot.ProactivePhrase` alert (Copilot file only) flags this directly — report it without re-deriving by judgment. - `Use proactively` in a Copilot description: Vale's `KyberforgeCopilot.ProactivePhrase` alert (Copilot file only) flags this directly — report it without re-deriving by judgment.
If a description finding is borderline, read `references/description-quality.md`. If a description finding is borderline, read `references/description-quality.md`.
@@ -62,7 +62,7 @@ If a description finding is borderline, read `references/description-quality.md`
- Direct role instruction: system prompt opens with `You are a [role]. When invoked, [action].` — SUGGESTION if absent - Direct role instruction: system prompt opens with `You are a [role]. When invoked, [action].` — SUGGESTION if absent
- One job per agent: system prompt describes a single bounded task — SUGGESTION if scope appears unbounded - One job per agent: system prompt describes a single bounded task — SUGGESTION if scope appears unbounded
- Generic, non-specific reference pointers to the `references/` directory: Vale's `Kyberforge.PaddingPhrase` alert flags this directly — report it without re-deriving by judgment - Generic, non-specific reference pointers to the `references/` directory: Vale's `Kyberforge.PaddingPhrase` alert flags this directly — report it without re-deriving by judgment
- Vague filler wording and sentences that open with "There is"/"There are": Vale's `Kyberforge.VagueQualifier` and `Kyberforge.SentenceOpenerThereIs` alerts flag this directly — report them without re-deriving by judgment - Sentences that open with "There is"/"There are": Vale's `Kyberforge.SentenceOpenerThereIs` alert flags this directly — report it without re-deriving by judgment
**Body/Frontmatter comments:** **Body/Frontmatter comments:**
- Inspect each comment block in the YAML frontmatter. For each comment, apply: *"Would the agent get this wrong without this comment?"* Flag any that answer "no" as padding. - Inspect each comment block in the YAML frontmatter. For each comment, apply: *"Would the agent get this wrong without this comment?"* Flag any that answer "no" as padding.

View File

@@ -41,7 +41,7 @@ Note any structural FAILs — they will appear in the report as a `### Structure
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. 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`, so it runs correctly regardless of the caller's cwd, using `.vale.ini`'s `Kyberforge` style — a deterministic prefilter for a subset of the Description/Patterns dimensions below, not a replacement for Step 3. Map `error` → `FAIL` and `warning`/`suggestion` → `SUGGESTION` in those dimensions, citing the rule ID (e.g. `Kyberforge.DescriptionOpener`). Skip and fall back to Step 3 judgment if vale or `.vale.ini` is unavailable. `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.
## Step 2 — Read all skill files ## Step 2 — Read all skill files
@@ -53,7 +53,7 @@ Work through each dimension internally. Collect findings only; report them in St
### Description ### Description
Vale's `Kyberforge.DescriptionOpener` (FAIL — "This skill..." openers) and `Kyberforge.VagueWording` (SUGGESTION — filler like "helps with", "utilize") alerts from Step 1 cover imperative phrasing and known vague-wording filler directly; report them as findings without re-deriving by judgment. The rest is still a judgment call: Vale's `Kyberforge.DescriptionOpener` ("This skill..." openers) and `Kyberforge.VagueWording` (filler like "helps with", "utilize") alerts from Step 1 — both FAILs — cover imperative phrasing and known vague-wording filler directly; report them as findings without re-deriving by judgment. The rest is still a judgment call:
- **Specificity beyond the filler blocklist**: are capabilities stated precisely ("parses OpenAPI specs") or genuinely vaguely ("handles files")? - **Specificity beyond the filler blocklist**: are capabilities stated precisely ("parses OpenAPI specs") or genuinely vaguely ("handles files")?
- **Indirect triggers**: does it cover cases where the user doesn't name the domain directly? - **Indirect triggers**: does it cover cases where the user doesn't name the domain directly?
@@ -70,7 +70,7 @@ For each sentence in the body, apply: *"Would the agent get this wrong without t
- **Why rationale**: include/exclude rules explain why, not just what - **Why rationale**: include/exclude rules explain why, not just what
- **Control calibration**: prescriptive for fragile or critical sequences (e.g. a script invocation where flag order or exact arguments must not change); flexible where multiple approaches are valid - **Control calibration**: prescriptive for fragile or critical sequences (e.g. a script invocation where flag order or exact arguments must not change); flexible where multiple approaches are valid
Vale's `Kyberforge.VagueQualifier` (SUGGESTION — vague filler like "clearly", "obviously") and `Kyberforge.SentenceOpenerThereIs` (SUGGESTION — sentences starting with "There is"/"There are") alerts from Step 1 cover pattern-matchable body-wide filler directly; report them as findings without re-deriving by judgment. Vale's `Kyberforge.SentenceOpenerThereIs` alert from Step 1 (FAIL — sentences starting with "There is"/"There are") covers pattern-matchable body-wide filler directly; report it as a finding without re-deriving by judgment.
If uncertain whether a sentence is padding or whether a control decision is correctly calibrated, read `references/body-discipline.md`. If uncertain whether a sentence is padding or whether a control decision is correctly calibrated, read `references/body-discipline.md`.

View File

@@ -19,9 +19,9 @@ metadata:
## Gotchas ## Gotchas
- Vale exits non-zero whenever it finds an alert at or above `MinAlertLevel` — that's what makes it usable as a CI gate, not a sign the invocation failed. Read the output before concluding the command errored. - Vale's exit code is driven by `error`-level alerts only. `warning` and `suggestion` alerts are reported but still exit `0`. `MinAlertLevel` and `--minAlertLevel` control display, never the exit code — no flag makes warnings fail. A rule that must gate CI or a commit hook has to be `level: error`. This is the single most common way a Vale gate silently passes everything.
- `vale ls-config` prints the fully-resolved, currently active configuration as JSON — the fastest way to check why a rule "isn't applying" is what's actually active, not what's written in `.vale.ini`. - `vale ls-config` prints the fully-resolved, currently active configuration as JSON — the fastest way to check why a rule "isn't applying" is what's actually active, not what's written in `.vale.ini`.
- Inline suppression syntax is format-specific: Markdown/MDX uses `{/* vale off */}` / `{/* vale on */}`, Org mode uses `# vale off` / `# vale on`. Don't assume one syntax works across formats. - Inline suppression syntax is format-specific: Markdown uses HTML comments `<!-- vale off -->` / `<!-- vale on -->`, MDX uses `{/* vale off */}` / `{/* vale on */}`, Org mode uses `# vale off` / `# vale on`. The MDX form does nothing in a plain `.md` file — the alert still fires. Don't assume one syntax works across formats.
- Before calling the `vale` binary directly, check whether the target repo documents its own wrapper script for Vale (look in its README, CONTRIBUTING docs, pre-commit config, or a `scripts/` directory). Some projects wrap `vale` to work around real bugs — e.g. a scope that silently stops matching multi-line YAML block-scalar frontmatter fields — and calling bare `vale` in a repo that has such a wrapper silently skips whatever the wrapper works around. If a wrapper is documented, invoke it with the same arguments instead of calling `vale` directly; otherwise fall back to the default below. - Before calling the `vale` binary directly, check whether the target repo documents its own wrapper script for Vale (look in its README, CONTRIBUTING docs, pre-commit config, or a `scripts/` directory). Some projects wrap `vale` to work around real bugs — e.g. a scope that silently stops matching multi-line YAML block-scalar frontmatter fields — and calling bare `vale` in a repo that has such a wrapper silently skips whatever the wrapper works around. If a wrapper is documented, invoke it with the same arguments instead of calling `vale` directly; otherwise fall back to the default below.
## Running vale ## Running vale
@@ -37,8 +37,8 @@ Key flags:
| Flag | Purpose | | Flag | Purpose |
|---|---| |---|---|
| `--output=<style>` | Output format/template: `CLI` (default, human-readable), `line` (compact, one alert per line, good for grep/piping), `JSON` (for programmatic parsing), or a custom template. | | `--output=<style>` | Output format/template: `CLI` (default, human-readable), `line` (compact, one alert per line, good for grep/piping), `JSON` (for programmatic parsing), or a custom template. |
| `--minAlertLevel=<suggestion\|warning\|error>` | Overrides `MinAlertLevel` from `.vale.ini` for this run only, without editing config. | | `--minAlertLevel=<suggestion\|warning\|error>` | Overrides `MinAlertLevel` from `.vale.ini` for this run only, without editing config. Filters what is displayed; does not affect the exit code. |
| `--no-exit` | Forces exit code `0` regardless of findings. Use in CI stages that should surface lint output without hard-failing the build. | | `--no-exit` | Suppresses the nonzero exit that `error`-level alerts would otherwise cause; a no-op when no rule is `error`-level. Use in CI stages that should surface lint output without hard-failing the build. |
| `--ignore-syntax` | Treats input as plain text, skipping format-aware parsing — use when a file's syntax-aware parser produces noisy or wrong results. | | `--ignore-syntax` | Treats input as plain text, skipping format-aware parsing — use when a file's syntax-aware parser produces noisy or wrong results. |
`vale sync` downloads the packages/styles declared in `.vale.ini` — that's a one-time-per-change setup step (vale-config's territory), not part of a normal lint run. If a run behaves as though no styles are active, that's a sign `vale sync` hasn't been run yet, not a `vale-run` problem. `vale sync` downloads the packages/styles declared in `.vale.ini` — that's a one-time-per-change setup step (vale-config's territory), not part of a normal lint run. If a run behaves as though no styles are active, that's a sign `vale sync` hasn't been run yet, not a `vale-run` problem.
@@ -49,14 +49,15 @@ Prefer `--output=JSON` whenever the caller (a script, a CI step, another agent)
Scope the fix as narrowly as possible, in this order: Scope the fix as narrowly as possible, in this order:
1. **One-off**: inline-suppress the specific text run with the format's `vale off`/`vale on` markup. 1. **Mentioning banned phrasing rather than using it**: wrap it in backticks or a fenced code block. Vale skips code spans and fences, so no suppression is needed at all. Try this before any suppression markup.
2. **Recurring known-exception string, one rule**: disable that specific rule for that specific match inline (e.g. `{/* vale Style.Redundancy["ACT test","OTHER"] = NO */}` ... `= YES`), rather than the whole rule. 2. **One-off**: inline-suppress the specific text run with the format's `vale off`/`vale on` markup.
3. **Known project term failing spell check**: add it to the style's `ignore` list, not an inline suppression. 3. **Recurring known-exception string, one rule**: disable that specific rule for that specific match inline (e.g. `<!-- vale Style.Redundancy["ACT test","OTHER"] = NO -->` ... `= YES`), rather than the whole rule.
4. **Known project term failing spell check**: add it to the style's `ignore` list, not an inline suppression.
Never disable a rule project-wide to fix one false positive — editing `.vale.ini`/`BasedOnStyles` is vale-config's job, and it silences the rule everywhere, not just the false-positive case. Never disable a rule project-wide to fix one false positive — editing `.vale.ini`/`BasedOnStyles` is vale-config's job, and it silences the rule everywhere, not just the false-positive case.
If output looks wrong because Vale mis-parsed a file's format, rerun with `--ignore-syntax` before assuming the rule itself is broken. If output looks wrong because Vale mis-parsed a file's format, rerun with `--ignore-syntax` before assuming the rule itself is broken.
For CI that fails solely because Vale returned non-zero on found alerts — not because the content is wrong for that pipeline stage — add `--no-exit` rather than disabling the rule. For CI that fails solely because Vale returned non-zero on `error`-level alerts — not because the content is wrong for that pipeline stage — add `--no-exit` rather than disabling the rule. If the failing alerts are warnings or suggestions, Vale is not what failed the build; look elsewhere.
If setting up Vale as a pre-commit hook or need the full inline-suppression/spelling-ignore syntax reference, read `references/troubleshooting.md`. If setting up Vale as a pre-commit hook or need the full inline-suppression/spelling-ignore syntax reference, read `references/troubleshooting.md`.

View File

@@ -7,7 +7,14 @@ source_keys:
## Inline suppression syntax by format ## Inline suppression syntax by format
Markdown/MDX: Markdown uses HTML comments — the MDX `{/* */}` form does not suppress anything in a plain `.md` file:
```markdown
<!-- vale off -->
This text will be ignored.
<!-- vale on -->
```
MDX:
```mdx ```mdx
{/* vale off */} {/* vale off */}
This text will be ignored. This text will be ignored.
@@ -25,6 +32,14 @@ This text will be ignored.
Targets one rule and specific known-exception strings, then re-enables — the preferred fix for a recurring false positive on a specific term, since it keeps the rule active everywhere else: Targets one rule and specific known-exception strings, then re-enables — the preferred fix for a recurring false positive on a specific term, since it keeps the rule active everywhere else:
Markdown:
```markdown
<!-- vale Style.Redundancy["ACT test","OTHER"] = NO -->
This is some text ACT test
<!-- vale Style.Redundancy["ACT test","OTHER"] = YES -->
```
MDX:
```mdx ```mdx
{/* vale Style.Redundancy["ACT test","OTHER"] = NO */} {/* vale Style.Redundancy["ACT test","OTHER"] = NO */}
This is some text ACT test This is some text ACT test
@@ -50,7 +65,7 @@ If a file's syntax-aware parsing produces noisy or incorrect results (an unsuppo
## CI failing unexpectedly ## CI failing unexpectedly
If a CI job fails solely because Vale returns a non-zero exit code on found alerts — not because the content is actually wrong for that pipeline stage — add `--no-exit` rather than suppressing the rule itself. This preserves the lint output while not gating the build on it. Only `error`-level alerts make Vale exit non-zero; `warning` and `suggestion` alerts are printed but exit `0`. If a CI job fails solely because of `error`-level alerts — not because the content is actually wrong for that pipeline stage — add `--no-exit` rather than suppressing the rule itself. This preserves the lint output while not gating the build on it. If the alerts are warnings or suggestions, Vale did not fail the job — look elsewhere.
## pre-commit integration ## pre-commit integration

View File

@@ -1,11 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Enforces agentskills.io's skill-authoring.md guidance: keep SKILL.md under 500 # Enforces agentskills.io's skill-authoring.md guidance: keep SKILL.md within
# lines and roughly 5,000 tokens, so the full body doesn't crowd out conversation # 500 lines and roughly 5,000 tokens, so the full body doesn't crowd out
# history and other active skills once loaded into context. Vale can't express # conversation history and other active skills once loaded into context. Vale
# a whole-file length ceiling (its checks operate on text patterns, not raw # can't express a whole-file length ceiling (its checks operate on text
# file size), so this is a plain script instead of a Vale rule. # patterns, not raw file size), so this is a plain script instead of a Vale
# rule.
#
# Both ceilings are inclusive: a file at exactly MAX_LINES or MAX_WORDS passes,
# and only one past it fails. That matches skill-audit/scripts/validate.sh,
# which has always used `line_count <= 500` as its pass condition — the two
# previously disagreed at exactly 500 lines, so a SKILL.md could pass its own
# audit and still be blocked by the commit hook.
# #
# Token counts aren't computed exactly here — word count (`wc -w`) is used as # Token counts aren't computed exactly here — word count (`wc -w`) is used as
# a proxy. This repo's own SKILL.md corpus measures ~5.7-6.5 characters per # a proxy. This repo's own SKILL.md corpus measures ~5.7-6.5 characters per
@@ -26,8 +33,8 @@ for f in "$@"; do
# Python's splitlines() semantics (used by skill-audit/scripts/validate.sh # Python's splitlines() semantics (used by skill-audit/scripts/validate.sh
# for its own line count) — `wc -l` undercounts by 1 in that case. # for its own line count) — `wc -l` undercounts by 1 in that case.
lines=$(awk 'END{print NR}' "$f") lines=$(awk 'END{print NR}' "$f")
if (( lines >= MAX_LINES )); then if (( lines > MAX_LINES )); then
echo "ERROR: $f has $lines lines, at or over the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2 echo "ERROR: $f has $lines lines, exceeding the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2
FAIL=1 FAIL=1
fi fi

View File

@@ -8,56 +8,104 @@ set -euo pipefail
# (padding with blank lines so every other line number is unchanged), then runs # (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 # the real `vale` binary against the copies. Drop-in replacement for calling
# `vale` directly: same args, same exit code. # `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.
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" cwd="$(pwd -P)"
vale_args=() vale_args=()
files=() file_args=()
config_next=false config_next=false
for arg in "$@"; do for arg in "$@"; do
if [[ "$config_next" == true ]]; then if [[ "$config_next" == true ]]; then
config_next=false
if [[ "$arg" == /* ]]; then if [[ "$arg" == /* ]]; then
vale_args+=("$arg") vale_args+=("$arg")
else else
vale_args+=("$repo_root/$arg") vale_args+=("$cwd/$arg")
fi fi
config_next=false
continue continue
fi fi
if [[ "$arg" == "--config" ]]; then case "$arg" in
vale_args+=("$arg") --config)
config_next=true vale_args+=("$arg")
continue config_next=true
fi continue
if [[ "$arg" == --config=* ]]; then ;;
cfg="${arg#--config=}" --config=/*)
if [[ "$cfg" == /* ]]; then vale_args+=("$arg")
vale_args+=("--config=$cfg") 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 else
vale_args+=("--config=$repo_root/$cfg") file_args+=("$arg")
fi fi
continue
fi
if [[ "$arg" != -* && -f "$repo_root/$arg" ]]; then
files+=("$arg")
elif [[ "$arg" == /* && -f "$arg" && "$arg" == "$repo_root"/* ]]; then
files+=("${arg#"$repo_root"/}")
else else
vale_args+=("$arg") vale_args+=("$arg")
fi fi
done done
if [[ ${#files[@]} -eq 0 ]]; then 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 exec vale "${vale_args[@]}" < /dev/null
fi fi
tmpdir="$(mktemp -d)" tmpdir="$(realpath -m "$(mktemp -d)")"
trap 'rm -rf "$tmpdir"' EXIT trap 'rm -rf "$tmpdir"' EXIT
for rel in "${files[@]}"; do # Mirror of the caller's cwd inside the scratch tree; relative file arguments
dest="$tmpdir/$rel" # 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")" mkdir -p "$(dirname "$dest")"
python3 - "$repo_root/$rel" "$dest" <<'PYTHON' python3 - "$arg" "$dest" <<'PYTHON'
import re import re
import sys import sys
@@ -118,7 +166,12 @@ if fm_match:
with open(dest, 'w') as fh: with open(dest, 'w') as fh:
fh.write(content) fh.write(content)
PYTHON PYTHON
if [[ "$arg" == /* ]]; then
argv_files+=("$dest")
else
argv_files+=("$arg")
fi
done done
cd "$tmpdir" cd "$mirror"
vale "${vale_args[@]}" "${files[@]}" vale "${vale_args[@]}" "${argv_files[@]}"

View File

@@ -1,6 +1,6 @@
extends: existence extends: existence
message: "Generic reference pointer: '%s' — use the specific 'If X, read `references/file.md`' form instead" message: "Generic reference pointer: '%s' — use the specific 'If X, read `references/file.md`' form instead"
level: warning level: error
scope: text scope: text
ignorecase: true ignorecase: true
raw: raw:

View File

@@ -1,6 +1,6 @@
extends: existence extends: existence
message: "Don't start a sentence with '%s' — name the subject directly" message: "Don't start a sentence with '%s' — name the subject directly"
level: warning level: error
scope: sentence scope: sentence
ignorecase: false ignorecase: false
raw: raw:

View File

@@ -1,19 +0,0 @@
extends: existence
message: "'%s' is vague filler wording — state the point precisely instead"
level: warning
scope: text
ignorecase: true
tokens:
- easily
- everyone knows
- exceedingly
- excellent
- extremely
- huge
- interestingly
- of course
- quite
- remarkably
- surprisingly
- vast
- very

View File

@@ -1,6 +1,6 @@
extends: existence extends: existence
message: "Vague capability wording: '%s' — state the capability precisely instead" message: "Vague capability wording: '%s' — state the capability precisely instead"
level: warning level: error
scope: text.frontmatter.description scope: text.frontmatter.description
ignorecase: true ignorecase: true
tokens: tokens:

View File

@@ -1,6 +1,6 @@
extends: existence extends: existence
message: "'%s' is CC-specific phrasing with no effect in Copilot descriptions — remove it" message: "'%s' is CC-specific phrasing with no effect in Copilot descriptions — remove it"
level: warning level: error
scope: text.frontmatter.description scope: text.frontmatter.description
ignorecase: true ignorecase: true
tokens: tokens:

View File

@@ -83,31 +83,33 @@ make_line_fixture() {
echo "$file" echo "$file"
} }
# The line ceiling is exclusive of the limit itself ("stay under $MAX_LINES # The line ceiling is inclusive of the limit itself, enforced via `>` — so
# lines", per skill-authoring.md), enforced via `>=` — so $((MAX_LINES - 1)) # exactly $MAX_LINES must pass and $((MAX_LINES + 1)) must fail. This matches
# must pass and $MAX_LINES itself must already fail. # skill-audit/scripts/validate.sh's `line_count <= 500` pass condition; the two
# previously disagreed at exactly $MAX_LINES lines, so a SKILL.md could pass its
# own audit and still be blocked by the commit hook.
echo "" echo ""
echo "--- passes a file at $((MAX_LINES - 1)) lines, just under the $MAX_LINES-line boundary ---" echo "--- passes a file at exactly the $MAX_LINES-line boundary ---"
AT_LINES="$(make_line_fixture at-line-limit "$((MAX_LINES - 1))")" AT_LINES="$(make_line_fixture at-line-limit "$MAX_LINES")"
ACTUAL_LINES=$(awk 'END{print NR}' "$AT_LINES") ACTUAL_LINES=$(awk 'END{print NR}' "$AT_LINES")
if [[ "$ACTUAL_LINES" -ne "$((MAX_LINES - 1))" ]]; then if [[ "$ACTUAL_LINES" -ne "$MAX_LINES" ]]; then
fail "fixture has $ACTUAL_LINES lines, expected exactly $((MAX_LINES - 1))" fail "fixture has $ACTUAL_LINES lines, expected exactly $MAX_LINES"
elif "$SCRIPT" "$AT_LINES"; then elif "$SCRIPT" "$AT_LINES"; then
pass "file at $((MAX_LINES - 1)) lines exits 0" pass "file at exactly $MAX_LINES lines exits 0"
else else
fail "file at $((MAX_LINES - 1)) lines should have exited 0" fail "file at exactly $MAX_LINES lines should have exited 0 (the off-by-one this test guards against)"
fi fi
echo "" echo ""
echo "--- fails a file at exactly the $MAX_LINES-line boundary ---" echo "--- fails a file one line over the $MAX_LINES-line boundary ---"
OVER_LINES="$(make_line_fixture over-line-limit "$MAX_LINES")" OVER_LINES="$(make_line_fixture over-line-limit "$((MAX_LINES + 1))")"
ACTUAL_OVER_LINES=$(awk 'END{print NR}' "$OVER_LINES") ACTUAL_OVER_LINES=$(awk 'END{print NR}' "$OVER_LINES")
if [[ "$ACTUAL_OVER_LINES" -ne "$MAX_LINES" ]]; then if [[ "$ACTUAL_OVER_LINES" -ne "$((MAX_LINES + 1))" ]]; then
fail "fixture has $ACTUAL_OVER_LINES lines, expected exactly $MAX_LINES" fail "fixture has $ACTUAL_OVER_LINES lines, expected exactly $((MAX_LINES + 1))"
elif "$SCRIPT" "$OVER_LINES" 2>/dev/null; then elif "$SCRIPT" "$OVER_LINES" 2>/dev/null; then
fail "file at exactly $MAX_LINES lines should have exited non-zero (>= ceiling, the boundary bug this test guards against)" fail "file at $((MAX_LINES + 1)) lines should have exited non-zero"
else else
pass "file at exactly $MAX_LINES lines exits non-zero" pass "file at $((MAX_LINES + 1)) lines exits non-zero"
fi fi
# make_word_fixture builds a file with an exact total word count (frontmatter # make_word_fixture builds a file with an exact total word count (frontmatter

View File

@@ -39,13 +39,24 @@ make_fixture() {
echo "$dir" echo "$dir"
} }
# Every Kyberforge rule is `level: error`, so vale exits non-zero whenever a
# fixture trips one — which is the expected outcome for nearly every case here.
# run_wrap therefore captures output and swallows the exit status; assertions
# are made on the report text. Cases that genuinely care about the exit code
# capture it explicitly instead.
run_wrap() {
local dir="$1"
shift
(cd "$dir" && bash "$SCRIPT" "$@" 2>&1) || true
}
# --- 1. A known-bad single-line description is caught (sanity check on Vale itself) --- # --- 1. A known-bad single-line description is caught (sanity check on Vale itself) ---
echo "" echo ""
echo "--- catches vague wording in a single-line description ---" echo "--- catches vague wording in a single-line description ---"
FIXTURE1="$(make_fixture 1)" FIXTURE1="$(make_fixture 1)"
trap 'rm -rf "$FIXTURE1"' EXIT trap 'rm -rf "$FIXTURE1"' EXIT
if (cd "$FIXTURE1" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \ if run_wrap "$FIXTURE1" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when description is a single physical line" pass "flags vague wording when description is a single physical line"
else else
fail "did not flag known-bad single-line description" fail "did not flag known-bad single-line description"
@@ -56,8 +67,8 @@ echo ""
echo "--- catches vague wording in a multi-line folded description ---" echo "--- catches vague wording in a multi-line folded description ---"
FIXTURE2="$(make_fixture 2)" FIXTURE2="$(make_fixture 2)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2"' EXIT
if (cd "$FIXTURE2" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \ if run_wrap "$FIXTURE2" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when description spans 2+ physical lines" pass "flags vague wording when description spans 2+ physical lines"
else else
fail "silently missed known-bad wording in a multi-line description — the bug this test guards against" fail "silently missed known-bad wording in a multi-line description — the bug this test guards against"
@@ -69,8 +80,8 @@ echo "--- preserves total line count when flattening ---"
FIXTURE3="$(make_fixture 3)" FIXTURE3="$(make_fixture 3)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3"' EXIT
ORIG_LINES=$(wc -l < "$FIXTURE3/plugins/testplugin/skills/zzzskill/SKILL.md") ORIG_LINES=$(wc -l < "$FIXTURE3/plugins/testplugin/skills/zzzskill/SKILL.md")
OUT=$(cd "$FIXTURE3" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \ OUT=$(run_wrap "$FIXTURE3" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1 || true) 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) 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 if [[ -n "$MAX_LINE" ]] && (( MAX_LINE <= ORIG_LINES )); then
pass "reported line numbers stay within the original file's line count" pass "reported line numbers stay within the original file's line count"
@@ -107,8 +118,8 @@ Body.
EOF EOF
)" )"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT
if (cd "$FIXTURE4" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \ if run_wrap "$FIXTURE4" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when the description contains a double quote" pass "flags vague wording when the description contains a double quote"
else else
fail "silently missed vague wording in a description containing a double quote — the bug this test guards against" fail "silently missed vague wording in a description containing a double quote — the bug this test guards against"
@@ -129,8 +140,8 @@ Body.
EOF EOF
)" )"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT
OUT5=$(cd "$FIXTURE5" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \ OUT5=$(run_wrap "$FIXTURE5" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1) plugins/testplugin/skills/zzzskill/SKILL.md)
if echo "$OUT5" | grep -q "VagueWording"; then if echo "$OUT5" | grep -q "VagueWording"; then
pass "flags vague wording when the description contains an apostrophe" pass "flags vague wording when the description contains an apostrophe"
else else
@@ -157,8 +168,8 @@ Body.
EOF EOF
)" )"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6"' EXIT
if (cd "$FIXTURE6" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \ if run_wrap "$FIXTURE6" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md) | grep -q "VagueWording"; then plugins/testplugin/skills/zzzskill/SKILL.md | grep -q "VagueWording"; then
pass "flags vague wording when the description has a backslash and non-ASCII text" pass "flags vague wording when the description has a backslash and non-ASCII text"
else else
fail "silently missed vague wording in a description with a backslash and non-ASCII text" fail "silently missed vague wording in a description with a backslash and non-ASCII text"
@@ -180,8 +191,8 @@ Body.
EOF EOF
)" )"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7"' EXIT
OUT7=$(cd "$FIXTURE7" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" \ OUT7=$(run_wrap "$FIXTURE7" --config "$REPO_ROOT/.vale.ini" \
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1) plugins/testplugin/skills/zzzskill/SKILL.md)
if echo "$OUT7" | grep -q "Traceback"; then if echo "$OUT7" | grep -q "Traceback"; then
fail "crashed while flattening a description with a blank line between paragraphs" fail "crashed while flattening a description with a blank line between paragraphs"
elif echo "$OUT7" | grep -q "VagueWording"; then elif echo "$OUT7" | grep -q "VagueWording"; then
@@ -190,9 +201,16 @@ else
fail "silently missed vague wording in the second paragraph after a blank line — the bug this test guards against" fail "silently missed vague wording in the second paragraph after a blank line — the bug this test guards against"
fi fi
# --- 8. --config=<path> (equals form) resolves the same as the two-argv form --- # --- 8. Relative paths resolve against the caller's cwd, exactly as bare vale
# resolves them. Every path below is deliberately relative to $SUBDIR8, not to
# the fixture's repo root: an earlier version of the wrapper resolved relative
# paths against the git toplevel instead, which (a) hard-errored on a
# `--config ../../..` that bare vale accepts and (b) silently dropped file
# arguments that didn't resolve from the repo root, skipping the flattening the
# wrapper exists to perform. The old tests only ever passed repo-root-relative
# paths from a subdirectory, so neither failure mode was caught.
echo "" echo ""
echo "--- --config=<path> equals form resolves from a subdirectory like the two-argv form ---" echo "--- resolves a cwd-relative --config from a subdirectory (equals and two-argv forms) ---"
FIXTURE8="$(mktemp -d)" FIXTURE8="$(mktemp -d)"
(cd "$FIXTURE8" && git init -q) (cd "$FIXTURE8" && git init -q)
cp "$REPO_ROOT/.vale.ini" "$FIXTURE8/.vale.ini" cp "$REPO_ROOT/.vale.ini" "$FIXTURE8/.vale.ini"
@@ -210,13 +228,53 @@ mkdir -p "$FIXTURE8/plugins/testplugin/skills/zzzskill"
} > "$FIXTURE8/plugins/testplugin/skills/zzzskill/SKILL.md" } > "$FIXTURE8/plugins/testplugin/skills/zzzskill/SKILL.md"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8"' EXIT
SUBDIR8="$FIXTURE8/plugins/testplugin/skills/zzzskill" SUBDIR8="$FIXTURE8/plugins/testplugin/skills/zzzskill"
REL8="plugins/testplugin/skills/zzzskill/SKILL.md" # Both paths are relative to $SUBDIR8 (four levels below the fixture root).
OUT_EQ=$(cd "$SUBDIR8" && bash "$SCRIPT" --config=.vale.ini "$REL8" 2>&1) REL_CFG8="../../../../.vale.ini"
OUT_TWO=$(cd "$SUBDIR8" && bash "$SCRIPT" --config .vale.ini "$REL8" 2>&1) REL_FILE8="SKILL.md"
OUT_EQ=$(run_wrap "$SUBDIR8" "--config=$REL_CFG8" "$REL_FILE8")
OUT_TWO=$(run_wrap "$SUBDIR8" --config "$REL_CFG8" "$REL_FILE8")
if echo "$OUT_EQ" | grep -q "VagueWording" && [[ "$OUT_EQ" == "$OUT_TWO" ]]; then if echo "$OUT_EQ" | grep -q "VagueWording" && [[ "$OUT_EQ" == "$OUT_TWO" ]]; then
pass "--config=<path> from a subdirectory resolves and matches the two-argv form" pass "cwd-relative --config resolves from a subdirectory in both argv forms"
else else
fail "--config=<path> equals form did not resolve the same as the two-argv form" fail "cwd-relative --config did not resolve from a subdirectory (equals form vs two-argv form)"
fi
# --- 8b. A cwd-relative --config matches the equivalent absolute invocation ---
# Regression for failure mode (a): resolving --config against the repo root made
# `--config ../../../../.vale.ini` expand to a path above the toplevel, and vale
# hard-errored with "does not exist" (exit 2) on args bare vale handles fine.
echo ""
echo "--- a cwd-relative --config produces the same result as the absolute-path form ---"
set +e
OUT_REL_CFG=$(cd "$SUBDIR8" && bash "$SCRIPT" --config "$REL_CFG8" "$REL_FILE8" 2>&1)
RC_REL_CFG=$?
OUT_ABS_CFG=$(cd "$SUBDIR8" && bash "$SCRIPT" --config "$FIXTURE8/.vale.ini" "$REL_FILE8" 2>&1)
RC_ABS_CFG=$?
set -e
if echo "$OUT_REL_CFG" | grep -qi "does not exist"; then
fail "cwd-relative --config hard-errored ('does not exist') — the bug this test guards against"
elif [[ "$OUT_REL_CFG" == "$OUT_ABS_CFG" && "$RC_REL_CFG" -eq "$RC_ABS_CFG" ]]; then
pass "cwd-relative --config matches the absolute-path invocation (output and exit code)"
else
fail "cwd-relative --config (rc=$RC_REL_CFG) diverged from the absolute-path form (rc=$RC_ABS_CFG)"
fi
# --- 8c. A cwd-relative FILE argument is still flattened, not silently skipped ---
# Regression for failure mode (b): a relative file path that didn't resolve from
# the repo root failed the wrapper's file test, fell through to the vale flag
# list, and left the file list empty — so the wrapper exec'd bare vale and
# silently skipped the flattening. Bare vale reports nothing here, so asserting
# on the alert (not just the exit code) is what makes the silence detectable.
echo ""
echo "--- flattens a cwd-relative file argument passed from a subdirectory ---"
WRAPPED_REL=$(run_wrap "$SUBDIR8" --config "$FIXTURE8/.vale.ini" "$REL_FILE8")
BARE_REL=$(cd "$SUBDIR8" && vale --config "$FIXTURE8/.vale.ini" "$REL_FILE8" 2>&1 || true)
if ! echo "$WRAPPED_REL" | grep -q "VagueWording"; then
fail "cwd-relative file argument produced no alert — flattening was silently skipped, the bug this test guards against"
elif echo "$BARE_REL" | grep -q "VagueWording"; then
fail "bare vale already flags this fixture, so the test can't detect a silently-skipped flattening"
else
pass "cwd-relative file argument is flattened and flagged where bare vale reports nothing"
fi fi
# --- 9. Zero file args (or a file list that filters to nothing) exits promptly --- # --- 9. Zero file args (or a file list that filters to nothing) exits promptly ---
@@ -252,10 +310,10 @@ echo "--- lints an absolute path to a skill file instead of silently skipping it
FIXTURE10="$(make_fixture 2)" FIXTURE10="$(make_fixture 2)"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10"' EXIT
ABS_FILE10="$FIXTURE10/plugins/testplugin/skills/zzzskill/SKILL.md" ABS_FILE10="$FIXTURE10/plugins/testplugin/skills/zzzskill/SKILL.md"
if (cd "$FIXTURE10" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" "$ABS_FILE10") | grep -q "VagueWording"; then if run_wrap "$FIXTURE10" --config "$REPO_ROOT/.vale.ini" "$ABS_FILE10" | grep -q "VagueWording"; then
pass "an absolute path under repo_root is linted, not silently skipped" pass "an absolute path is linted, not silently skipped"
else else
fail "an absolute path under repo_root was silently skipped — the bug this test guards against" fail "an absolute path was silently skipped — the bug this test guards against"
fi fi
# --- 11. A literal (|) block scalar passes through unflattened (no regression) --- # --- 11. A literal (|) block scalar passes through unflattened (no regression) ---
@@ -274,7 +332,7 @@ EOF
)" )"
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11"' EXIT trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11"' EXIT
REL11="plugins/testplugin/skills/zzzskill/SKILL.md" REL11="plugins/testplugin/skills/zzzskill/SKILL.md"
WRAPPED_OUT=$(cd "$FIXTURE11" && bash "$SCRIPT" --config "$REPO_ROOT/.vale.ini" "$REL11" 2>&1 || true) 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) BARE_OUT=$(cd "$FIXTURE11" && vale --config "$REPO_ROOT/.vale.ini" "$REL11" 2>&1 || true)
if [[ "$WRAPPED_OUT" == "$BARE_OUT" ]]; then if [[ "$WRAPPED_OUT" == "$BARE_OUT" ]]; then
pass "literal (|) block scalar output matches bare vale exactly — untouched by flattening" pass "literal (|) block scalar output matches bare vale exactly — untouched by flattening"