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:
@@ -61,6 +61,15 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
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
|
- id: validate-plugins
|
||||||
name: Validate plugins
|
name: Validate plugins
|
||||||
description: Run claude plugin validate --strict on every plugin directory
|
description: Run claude plugin validate --strict on every plugin directory
|
||||||
@@ -107,13 +116,22 @@ repos:
|
|||||||
files: '^plugins/[^/]+/skills/[^/]+/SKILL\.md$'
|
files: '^plugins/[^/]+/skills/[^/]+/SKILL\.md$'
|
||||||
pass_filenames: true
|
pass_filenames: true
|
||||||
|
|
||||||
- id: vale-audit-prefilter
|
- id: vale-audit-prefilter-skill
|
||||||
stages: ['pre-commit']
|
stages: ['pre-commit']
|
||||||
name: Vale audit prefilter
|
name: Vale audit prefilter (SKILL.md)
|
||||||
description: Run Vale against skill/agent markdown files as a deterministic prefilter for skill-audit/agent-audit
|
description: Run Vale against SKILL.md files as a deterministic prefilter for skill-audit, via skill-audit's own bundled copy
|
||||||
entry: scripts/vale-wrap.sh --config .vale.ini
|
entry: plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh --config plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini
|
||||||
language: script
|
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
|
pass_filenames: true
|
||||||
|
|
||||||
- repo: meta
|
- repo: meta
|
||||||
|
|||||||
20
.pre-commit-hooks.yaml
Normal file
20
.pre-commit-hooks.yaml
Normal 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$'
|
||||||
@@ -22,7 +22,7 @@ Fall back to raw shell only when no skill covers it.
|
|||||||
## Setup and testing
|
## 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 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.
|
- 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.
|
- 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.
|
- Author commits with `git:git-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.
|
||||||
|
|||||||
@@ -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. 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
|
### 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.
|
||||||
|
|||||||
@@ -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"
|
## 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
|
## 2026-08-08 — One signal, two consumers, no named distinction
|
||||||
|
|
||||||
|
|||||||
99
docs/adr/0014-vale-prefilter-ships-from-the-plugin.md
Normal file
99
docs/adr/0014-vale-prefilter-ships-from-the-plugin.md
Normal 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.
|
||||||
@@ -8,5 +8,5 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"name": "kyberforge",
|
"name": "kyberforge",
|
||||||
"version": "1.2.4"
|
"version": "1.2.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"skills/"
|
"skills/"
|
||||||
],
|
],
|
||||||
"version": "1.2.4"
|
"version": "1.2.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ metadata:
|
|||||||
```bash
|
```bash
|
||||||
bash scripts/validate.sh <path-to-agent-file>
|
bash scripts/validate.sh <path-to-agent-file>
|
||||||
bash scripts/validate-provenance.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`.
|
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.
|
`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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
StylesPath = styles
|
StylesPath = styles
|
||||||
|
|
||||||
[**/SKILL.md]
|
|
||||||
BasedOnStyles = Kyberforge
|
|
||||||
|
|
||||||
[**/agents/*.md]
|
[**/agents/*.md]
|
||||||
BasedOnStyles = Kyberforge
|
BasedOnStyles = Kyberforge
|
||||||
|
|
||||||
@@ -34,14 +34,14 @@ metadata:
|
|||||||
```bash
|
```bash
|
||||||
bash scripts/validate.sh <skill-dir>
|
bash scripts/validate.sh <skill-dir>
|
||||||
bash scripts/validate-provenance.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 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.
|
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
|
## Step 2 — Read all skill files
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
StylesPath = styles
|
||||||
|
|
||||||
|
[**/SKILL.md]
|
||||||
|
BasedOnStyles = Kyberforge
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||
@@ -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
|
||||||
177
plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh
Executable file
177
plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh
Executable 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[@]}"
|
||||||
35
scripts/check-vale-style-sync.sh
Executable file
35
scripts/check-vale-style-sync.sh
Executable 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
|
||||||
91
tests/test-check-vale-style-sync.sh
Executable file
91
tests/test-check-vale-style-sync.sh
Executable 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 ]]
|
||||||
@@ -6,7 +6,12 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
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
|
PASS=0
|
||||||
FAIL=0
|
FAIL=0
|
||||||
|
|
||||||
@@ -55,7 +60,7 @@ 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 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
|
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
|
||||||
@@ -67,7 +72,7 @@ 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 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
|
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
|
||||||
@@ -80,7 +85,7 @@ 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=$(run_wrap "$FIXTURE3" --config "$REPO_ROOT/.vale.ini" \
|
OUT=$(run_wrap "$FIXTURE3" --config "$VALE_CONFIG" \
|
||||||
plugins/testplugin/skills/zzzskill/SKILL.md)
|
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
|
||||||
@@ -118,7 +123,7 @@ Body.
|
|||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4"' EXIT
|
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
|
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
|
||||||
@@ -140,7 +145,7 @@ Body.
|
|||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5"' EXIT
|
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)
|
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"
|
||||||
@@ -168,7 +173,7 @@ Body.
|
|||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6"' EXIT
|
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
|
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
|
||||||
@@ -191,7 +196,7 @@ 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=$(run_wrap "$FIXTURE7" --config "$REPO_ROOT/.vale.ini" \
|
OUT7=$(run_wrap "$FIXTURE7" --config "$VALE_CONFIG" \
|
||||||
plugins/testplugin/skills/zzzskill/SKILL.md)
|
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"
|
||||||
@@ -213,8 +218,8 @@ echo ""
|
|||||||
echo "--- resolves a cwd-relative --config from a subdirectory (equals and two-argv forms) ---"
|
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 "$VALE_CONFIG" "$FIXTURE8/.vale.ini"
|
||||||
cp -r "$REPO_ROOT/styles" "$FIXTURE8/styles"
|
cp -r "$SKILL_AUDIT/assets/vale/styles" "$FIXTURE8/styles"
|
||||||
mkdir -p "$FIXTURE8/plugins/testplugin/skills/zzzskill"
|
mkdir -p "$FIXTURE8/plugins/testplugin/skills/zzzskill"
|
||||||
{
|
{
|
||||||
echo "---"
|
echo "---"
|
||||||
@@ -280,7 +285,7 @@ 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 ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- exits promptly instead of hanging on stdin when no files are passed ---"
|
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"
|
pass "exits promptly with zero file args"
|
||||||
else
|
else
|
||||||
RC=$?
|
RC=$?
|
||||||
@@ -293,7 +298,7 @@ fi
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- exits promptly when a file list filters down to nothing ---"
|
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"
|
pass "exits promptly when no file-shaped args remain"
|
||||||
else
|
else
|
||||||
RC=$?
|
RC=$?
|
||||||
@@ -310,7 +315,7 @@ 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 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"
|
pass "an absolute path is linted, not silently skipped"
|
||||||
else
|
else
|
||||||
fail "an absolute path was silently skipped — the bug this test guards against"
|
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
|
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=$(run_wrap "$FIXTURE11" --config "$REPO_ROOT/.vale.ini" "$REL11")
|
WRAPPED_OUT=$(run_wrap "$FIXTURE11" --config "$VALE_CONFIG" "$REL11")
|
||||||
BARE_OUT=$(cd "$FIXTURE11" && vale --config "$REPO_ROOT/.vale.ini" "$REL11" 2>&1 || true)
|
BARE_OUT=$(cd "$FIXTURE11" && vale --config "$VALE_CONFIG" "$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"
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user