Compare commits
5 Commits
acd2f1d422
...
cc5f366450
| Author | SHA1 | Date | |
|---|---|---|---|
| cc5f366450 | |||
| e9234f6d8a | |||
| 348dd9f665 | |||
| 714e8a0c78 | |||
| 8c570e9659 |
@@ -129,7 +129,7 @@ repos:
|
|||||||
stages: ['pre-commit']
|
stages: ['pre-commit']
|
||||||
name: Vale audit prefilter (SKILL.md)
|
name: Vale audit prefilter (SKILL.md)
|
||||||
description: Run Vale against SKILL.md files as a deterministic prefilter for skill-audit, via skill-audit's own bundled copy
|
description: Run Vale against SKILL.md files as a deterministic prefilter for skill-audit, via skill-audit's own bundled copy
|
||||||
entry: plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh --config plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini
|
entry: plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh
|
||||||
language: script
|
language: script
|
||||||
files: '^plugins/[^/]+/skills/[^/]+/SKILL\.md$'
|
files: '^plugins/[^/]+/skills/[^/]+/SKILL\.md$'
|
||||||
pass_filenames: true
|
pass_filenames: true
|
||||||
@@ -138,7 +138,7 @@ repos:
|
|||||||
stages: ['pre-commit']
|
stages: ['pre-commit']
|
||||||
name: Vale audit prefilter (agent files)
|
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
|
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
|
entry: plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh
|
||||||
language: script
|
language: script
|
||||||
files: '^plugins/[^/]+/agents/[^/]+\.md$'
|
files: '^plugins/[^/]+/agents/[^/]+\.md$'
|
||||||
pass_filenames: true
|
pass_filenames: true
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
- id: kyberforge-vale-audit-skill
|
- id: kyberforge-vale-audit-skill
|
||||||
name: Kyberforge Vale prose audit (SKILL.md)
|
name: Kyberforge Vale prose audit (SKILL.md)
|
||||||
description: Deterministic prose-pattern prefilter for kyberforge's skill-audit, via its own bundled Vale config/styles
|
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
|
entry: plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh
|
||||||
language: script
|
language: script
|
||||||
files: '(^|/)SKILL\.md$'
|
files: '(^|/)SKILL\.md$'
|
||||||
|
|
||||||
- id: kyberforge-vale-audit-agent
|
- id: kyberforge-vale-audit-agent
|
||||||
name: Kyberforge Vale prose audit (agent files)
|
name: Kyberforge Vale prose audit (agent files)
|
||||||
description: Deterministic prose-pattern prefilter for kyberforge's agent-audit, via its own bundled Vale config/styles
|
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
|
entry: plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh
|
||||||
language: script
|
language: script
|
||||||
files: '(^|/)agents/[^/]+\.md$|\.agent\.md$'
|
files: '(^|/)agents/[^/]+\.md$|\.agent\.md$'
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ A standalone, repo-agnostic plugin (`plugins/lint/`) for configuring and running
|
|||||||
### 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 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.
|
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-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.
|
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. Handed no `--config` at all, the wrapper falls back to its own sibling `assets/vale/.vale.ini`, located from `${BASH_SOURCE[0]}` rather than from the cwd — which is why both manifests' `entry:` is now the bare script path with no argument after it. pre-commit prefixes only `entry[0]` with the hook-repo clone path (`cmd = (prefix.path(cmd[0]), *cmd[1:])`), so every later argument resolves against the *consuming* repo's root: a `--config` in `.pre-commit-hooks.yaml` pointed at a path no consumer has and hard-failed every external run with `E100 [--config] Runtime error`. `.pre-commit-config.yaml` drops the argument too, deliberately keeping the two entries identical — the local `repo: local` hook resolved its `--config` correctly only because the consuming repo *was* this repo, and that divergence is why three review rounds exercised a path no external consumer takes and missed the defect. An explicit `--config` still wins, in all three argv forms (`--config X`, `--config=/abs`, `--config=rel`), and a relative one still resolves against the caller's cwd, matching bare `vale`, not the repo root; both audit skills' Step 1 still passes `--config assets/vale/.vale.ini` and is unaffected. `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 and from the audit skills passing one explicit file per invocation. The two manifests scope differently on purpose: this repo's `.pre-commit-config.yaml` pins its own layout — `^plugins/[^/]+/skills/[^/]+/SKILL\.md$` for `-skill`, `^plugins/[^/]+/agents/[^/]+\.md$` for `-agent` — while the shipped `.pre-commit-hooks.yaml` stays layout-agnostic for external consumers whose skills live anywhere, using `(^|/)SKILL\.md$` and `(^|/)agents/[^/]+\.md$|\.agent\.md$`. Both manifests split the prefilter into two hooks precisely because one combined hook pointed at only one copy would silently 0-file-skip the other file type. A `SKILL.md` outside `plugins/` (e.g. project-scope `.claude/skills/foo/SKILL.md`) still matches `[**/SKILL.md]` and gets linted normally — the globs constrain filename shape, not location. Vale reports 0 files only when the path it is handed matches no glob section at all: a differently-named file, or a directory argument holding nothing that matches. That run prints `✔ 0 errors ... in 0 files.` and exits 0, indistinguishable from a clean pass, so 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-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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,27 @@ define separate `-skill`/`-agent` hook IDs, each with a `files:` regex matching
|
|||||||
target copy's glob covers. (Confirmed empirically before deleting the root files: retargeting a
|
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.)
|
single hook at agent-audit's copy silently scanned 0 SKILL.md files.)
|
||||||
|
|
||||||
|
**The hook `entry:` is the wrapper alone; the wrapper self-locates its config.** pre-commit
|
||||||
|
prefixes only `entry[0]` with the hook-repo clone path (`cmd = (prefix.path(cmd[0]), *cmd[1:])`);
|
||||||
|
every later argument is handed to the process untouched and so resolves against the *consuming*
|
||||||
|
repo's root. A `--config plugins/kyberforge/skills/…/assets/vale/.vale.ini` in
|
||||||
|
`.pre-commit-hooks.yaml` therefore named a path no consumer has, and every external run died with
|
||||||
|
`E100 [--config] Runtime error`. The external-consumer contract this ADR exists to establish
|
||||||
|
cannot be expressed as a `--config` argument at all — the config path has to be derived inside
|
||||||
|
the process, from the script's own location. `vale-wrap.sh` accordingly defaults to its sibling
|
||||||
|
`assets/vale/.vale.ini`, resolved from `${BASH_SOURCE[0]}`, whenever no `--config` is supplied;
|
||||||
|
an explicit `--config` from any other caller still wins and still resolves against the caller's
|
||||||
|
cwd, so both audit skills' Step 1 (`--config assets/vale/.vale.ini`) is unaffected. Both
|
||||||
|
manifests now carry the identical argument-free `entry:`. Keeping them identical is part of the
|
||||||
|
decision: the local `repo: local` hook resolved its `--config` correctly only because the
|
||||||
|
consuming repo *was* this repo, and that one difference is why three review rounds exercised a
|
||||||
|
code path no external consumer ever takes.
|
||||||
|
|
||||||
**Vale's `StylesPath` resolves relative to the `.vale.ini` file's own location**, confirmed
|
**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
|
against `docs.vale.sh/keys/stylespath` — so a config path into the plugin finds that ini's
|
||||||
finds that ini's sibling `styles/` regardless of the caller's cwd, with no extra path-juggling
|
sibling `styles/` regardless of the caller's cwd, whether it arrives as an explicit `--config` or
|
||||||
needed beyond what `vale-wrap.sh` already does for its cwd-relative `--config`/file-argument
|
as the wrapper's self-located default. No extra path-juggling is needed beyond `vale-wrap.sh`'s
|
||||||
handling.
|
cwd-relative `--config`/path-argument handling and that fallback.
|
||||||
|
|
||||||
**A sync-check catches drift between the two copies.** `scripts/check-vale-style-sync.sh` diffs
|
**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
|
`scripts/vale-wrap.sh` and `assets/vale/styles/Kyberforge/` between skill-audit and agent-audit
|
||||||
@@ -91,8 +107,18 @@ doesn't wonder if it was overlooked.
|
|||||||
- Root `.vale.ini`, `styles/`, `scripts/vale-wrap.sh` are deleted. Two copies remain:
|
- 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/agent-audit/assets/vale/` (canonical, superset) and
|
||||||
`plugins/kyberforge/skills/skill-audit/assets/vale/` (subset, `Kyberforge` only).
|
`plugins/kyberforge/skills/skill-audit/assets/vale/` (subset, `Kyberforge` only).
|
||||||
- `plugins/kyberforge`'s `plugin.json` and `.claude-plugin/plugin.json` both patch-bump to
|
- `plugins/kyberforge`'s `plugin.json` and `.claude-plugin/plugin.json` both patch-bump for every
|
||||||
`1.2.5` for the shipped content change (per ADR-0006's version-parity invariant).
|
shipped content change (per ADR-0006's version-parity invariant): `1.2.5` for the relocation
|
||||||
|
itself, `1.2.6` for the self-locating `vale-wrap.sh` that followed.
|
||||||
|
- **`.pre-commit-hooks.yaml` entries are a bare script path and nothing else — a constraint, not a
|
||||||
|
house style, and it binds every future hook here, not just the Vale two.** Since pre-commit
|
||||||
|
rewrites only `entry[0]` into the hook-repo clone, no argument token in any entry can reference
|
||||||
|
a file this repo ships: a relative path resolves against the *consuming* repo and hard-fails,
|
||||||
|
and the absolute path is unknowable at author time. A hook that needs one of its own bundled
|
||||||
|
files must have the script self-locate it from `$0`/`${BASH_SOURCE[0]}`, exactly as
|
||||||
|
`vale-wrap.sh` now does for `.vale.ini`. Anything else rediscovers this as another `E100`.
|
||||||
|
`.pre-commit-config.yaml` stays byte-identical to the shipped manifest on those `entry:` lines
|
||||||
|
so the local gate keeps exercising the same resolution path a consumer does.
|
||||||
- `tests/test-vale-wrap.sh` now exercises skill-audit's copy specifically — its fixtures are all
|
- `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.
|
`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
|
- The first `vX.Y.Z` tag is cut once this change and its tests pass, giving external
|
||||||
@@ -117,3 +143,14 @@ doesn't wonder if it was overlooked.
|
|||||||
as a separate piece of infrastructure, not fixed here. `RELEASE_PATHS` is derived from
|
as a separate piece of infrastructure, not fixed here. `RELEASE_PATHS` is derived from
|
||||||
`.pre-commit-hooks.yaml`'s own `entry:` lines rather than hand-maintained, so at least the set of
|
`.pre-commit-hooks.yaml`'s own `entry:` lines rather than hand-maintained, so at least the set of
|
||||||
paths it checks can't drift from the manifest on its own.
|
paths it checks can't drift from the manifest on its own.
|
||||||
|
- **Dropping `--config` moved the release gate's path derivation too.** `check-release-needed.sh`
|
||||||
|
used to reach each hook's bundled assets through the `dirname` of its `--config` target. With
|
||||||
|
no `--config` token left, that loop went dead and silently dropped both `assets/vale/` trees
|
||||||
|
from release coverage — a Vale *rule* change could then land on `main` without demanding a tag,
|
||||||
|
leaving consumers pinned to an old `rev:` running stale rules while the gate stayed green. The
|
||||||
|
script now derives the bundle's `assets/` tree from `tokens[0]` instead (double-`dirname`,
|
||||||
|
guarded on the candidate existing and on not resolving to `.`), which is the only derivation
|
||||||
|
compatible with the argument-free `entry:` contract above.
|
||||||
|
- **Accepted residual in the release gate:** deleting a hook's *entire* `assets/` tree is not
|
||||||
|
flagged — the derived candidate path stops existing, so the guard drops it before it reaches
|
||||||
|
the pathspec. Deleting individual files inside a surviving tree is flagged, and tested.
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"name": "kyberforge",
|
"name": "kyberforge",
|
||||||
"version": "1.2.5"
|
"version": "1.2.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"skills/"
|
"skills/"
|
||||||
],
|
],
|
||||||
"version": "1.2.5"
|
"version": "1.2.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,26 +9,35 @@ set -euo pipefail
|
|||||||
# 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
|
# "Same args" means relative paths — `--config` values and path arguments alike
|
||||||
# — resolve against the caller's current directory, exactly as bare `vale`
|
# — resolve against the caller's current directory, exactly as bare `vale`
|
||||||
# resolves them. (An earlier version resolved them against the repo root, an
|
# resolves them. (An earlier version resolved them against the repo root, an
|
||||||
# invented convention that hard-errored on `--config ../../.vale.ini` from a
|
# invented convention that hard-errored on `--config ../../.vale.ini` from a
|
||||||
# subdirectory and, worse, silently dropped file arguments that didn't happen to
|
# subdirectory and, worse, silently dropped file arguments that didn't happen to
|
||||||
# resolve from the repo root — skipping the flattening this script exists for.)
|
# 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
|
# The one addition to bare `vale`'s argument handling: with no `--config` at
|
||||||
# mirrors the caller's absolute cwd: a relative file argument is passed through
|
# all, this script's own sibling `assets/vale/.vale.ini` is used instead of
|
||||||
|
# vale's upward search. pre-commit prefixes only `entry[0]` with the hook-repo
|
||||||
|
# clone path, so a `--config` in `.pre-commit-hooks.yaml` would resolve against
|
||||||
|
# the *consuming* repo and hard-fail (E100) for every external consumer. The
|
||||||
|
# manifest therefore passes the script alone, and an explicit `--config` from
|
||||||
|
# any other caller still wins.
|
||||||
|
#
|
||||||
|
# Vale prints each path exactly as it was handed to it, so the scratch tree
|
||||||
|
# mirrors the caller's absolute cwd: a relative path argument is passed through
|
||||||
# verbatim and resolves to its flattened copy, keeping the report byte-identical
|
# 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
|
# to bare `vale`'s. An absolute path inside the cwd is relativized to keep that
|
||||||
# keep that property. Only an absolute path outside the cwd is rewritten to its
|
# property. Only an absolute path outside the cwd is rewritten to its scratch
|
||||||
# scratch copy and so reports a scratch path — unavoidable, since a file can
|
# copy and so reports a scratch path — unavoidable, since a file can only be
|
||||||
# only be read from where it actually is.
|
# read from where it actually is.
|
||||||
|
|
||||||
cwd="$(pwd -P)"
|
cwd="$(pwd -P)"
|
||||||
|
|
||||||
vale_args=()
|
vale_args=()
|
||||||
file_args=()
|
path_args=()
|
||||||
config_next=false
|
config_next=false
|
||||||
|
config_given=false
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
if [[ "$config_next" == true ]]; then
|
if [[ "$config_next" == true ]]; then
|
||||||
config_next=false
|
config_next=false
|
||||||
@@ -43,19 +52,24 @@ for arg in "$@"; do
|
|||||||
--config)
|
--config)
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
config_next=true
|
config_next=true
|
||||||
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
--config=/*)
|
--config=/*)
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
--config=*)
|
--config=*)
|
||||||
vale_args+=("--config=$cwd/${arg#--config=}")
|
vale_args+=("--config=$cwd/${arg#--config=}")
|
||||||
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
# `-f` resolves relative paths against the caller's cwd, same as vale does.
|
# `-f`/`-d` resolve relative paths against the caller's cwd, same as vale does.
|
||||||
if [[ "$arg" != -* && -f "$arg" ]]; then
|
# A path that doesn't exist is left for vale to report on, exactly as bare
|
||||||
|
# vale would.
|
||||||
|
if [[ "$arg" != -* && ( -f "$arg" || -d "$arg" ) ]]; then
|
||||||
# An absolute path inside the caller's cwd is relativized so the report cites
|
# 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
|
# 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
|
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
|
||||||
@@ -63,54 +77,41 @@ for arg in "$@"; do
|
|||||||
# quoting it. Absolute paths outside the cwd have no relative form and keep
|
# quoting it. Absolute paths outside the cwd have no relative form and keep
|
||||||
# the scratch-path behaviour documented above.
|
# the scratch-path behaviour documented above.
|
||||||
if [[ "$arg" == "$cwd"/* ]]; then
|
if [[ "$arg" == "$cwd"/* ]]; then
|
||||||
file_args+=("${arg#"$cwd"/}")
|
path_args+=("${arg#"$cwd"/}")
|
||||||
else
|
else
|
||||||
file_args+=("$arg")
|
path_args+=("$arg")
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ${#file_args[@]} -eq 0 ]]; then
|
if [[ "$config_given" == false ]]; then
|
||||||
|
vale_args+=(--config "$(cd "$(dirname "${BASH_SOURCE[0]}")/../assets/vale" && pwd)/.vale.ini")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#path_args[@]} -eq 0 ]]; then
|
||||||
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
||||||
# block waiting on a pipe that will never carry content.
|
# 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="$(realpath -m "$(mktemp -d)")"
|
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
|
||||||
trap 'rm -rf "$tmpdir"' EXIT
|
# is a GNU extension the BSD realpath on macOS doesn't have — and every dest
|
||||||
|
# below is a path that doesn't exist yet. python3 is already a hard dependency.
|
||||||
|
abspath() {
|
||||||
|
python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
# Mirror of the caller's cwd inside the scratch tree; relative file arguments
|
flatten() {
|
||||||
# are resolved from here.
|
python3 - "$1" "$2" <<'PYTHON'
|
||||||
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 re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
src, dest = sys.argv[1], sys.argv[2]
|
src, dest = sys.argv[1], sys.argv[2]
|
||||||
with open(src) as fh:
|
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
|
||||||
|
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
|
||||||
|
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
content = fh.read()
|
content = fh.read()
|
||||||
|
|
||||||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||||||
@@ -163,15 +164,60 @@ if fm_match:
|
|||||||
new_fm = fm[:start] + f'description: {flat_q}\n{pad}' + fm[end:]
|
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():]
|
content = fm_match.group(1) + new_fm + fm_match.group(3) + content[fm_match.end():]
|
||||||
|
|
||||||
with open(dest, 'w') as fh:
|
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
fh.write(content)
|
fh.write(content)
|
||||||
PYTHON
|
PYTHON
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpdir="$(cd "$(mktemp -d)" && pwd -P)"
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
|
||||||
|
# Mirror of the caller's cwd inside the scratch tree; relative path arguments
|
||||||
|
# are resolved from here.
|
||||||
|
mirror="$tmpdir$cwd"
|
||||||
|
mkdir -p "$mirror"
|
||||||
|
|
||||||
|
argv_paths=()
|
||||||
|
for arg in "${path_args[@]}"; do
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
argv_files+=("$dest")
|
dest="$tmpdir$arg"
|
||||||
else
|
else
|
||||||
argv_files+=("$arg")
|
dest="$mirror/$arg"
|
||||||
|
fi
|
||||||
|
dest="$(abspath "$dest")"
|
||||||
|
# A path 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")"
|
||||||
|
if [[ -d "$arg" ]]; then
|
||||||
|
# A directory is mirrored whole — vale applies its own format filtering to
|
||||||
|
# the tree, so any file dropped here would be silently unlinted — and then
|
||||||
|
# every markdown file in the copy is flattened in place. `.git` is pruned:
|
||||||
|
# vale never lints it and copying it can dwarf the rest of the tree.
|
||||||
|
mkdir -p "$dest"
|
||||||
|
while IFS= read -r -d '' rel; do
|
||||||
|
mkdir -p "$dest/$(dirname "$rel")"
|
||||||
|
cp "$arg/$rel" "$dest/$rel"
|
||||||
|
done < <(cd "$arg" && find . -name .git -prune -o -type f -print0)
|
||||||
|
while IFS= read -r -d '' md; do
|
||||||
|
flatten "$md" "$md"
|
||||||
|
done < <(find "$dest" -type f -name '*.md' -print0)
|
||||||
|
else
|
||||||
|
flatten "$arg" "$dest"
|
||||||
|
fi
|
||||||
|
if [[ "$arg" == /* ]]; then
|
||||||
|
argv_paths+=("$dest")
|
||||||
|
else
|
||||||
|
argv_paths+=("$arg")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
cd "$mirror"
|
cd "$mirror"
|
||||||
vale "${vale_args[@]}" "${argv_files[@]}"
|
vale "${vale_args[@]}" "${argv_paths[@]}"
|
||||||
|
|||||||
@@ -9,26 +9,35 @@ set -euo pipefail
|
|||||||
# 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
|
# "Same args" means relative paths — `--config` values and path arguments alike
|
||||||
# — resolve against the caller's current directory, exactly as bare `vale`
|
# — resolve against the caller's current directory, exactly as bare `vale`
|
||||||
# resolves them. (An earlier version resolved them against the repo root, an
|
# resolves them. (An earlier version resolved them against the repo root, an
|
||||||
# invented convention that hard-errored on `--config ../../.vale.ini` from a
|
# invented convention that hard-errored on `--config ../../.vale.ini` from a
|
||||||
# subdirectory and, worse, silently dropped file arguments that didn't happen to
|
# subdirectory and, worse, silently dropped file arguments that didn't happen to
|
||||||
# resolve from the repo root — skipping the flattening this script exists for.)
|
# 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
|
# The one addition to bare `vale`'s argument handling: with no `--config` at
|
||||||
# mirrors the caller's absolute cwd: a relative file argument is passed through
|
# all, this script's own sibling `assets/vale/.vale.ini` is used instead of
|
||||||
|
# vale's upward search. pre-commit prefixes only `entry[0]` with the hook-repo
|
||||||
|
# clone path, so a `--config` in `.pre-commit-hooks.yaml` would resolve against
|
||||||
|
# the *consuming* repo and hard-fail (E100) for every external consumer. The
|
||||||
|
# manifest therefore passes the script alone, and an explicit `--config` from
|
||||||
|
# any other caller still wins.
|
||||||
|
#
|
||||||
|
# Vale prints each path exactly as it was handed to it, so the scratch tree
|
||||||
|
# mirrors the caller's absolute cwd: a relative path argument is passed through
|
||||||
# verbatim and resolves to its flattened copy, keeping the report byte-identical
|
# 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
|
# to bare `vale`'s. An absolute path inside the cwd is relativized to keep that
|
||||||
# keep that property. Only an absolute path outside the cwd is rewritten to its
|
# property. Only an absolute path outside the cwd is rewritten to its scratch
|
||||||
# scratch copy and so reports a scratch path — unavoidable, since a file can
|
# copy and so reports a scratch path — unavoidable, since a file can only be
|
||||||
# only be read from where it actually is.
|
# read from where it actually is.
|
||||||
|
|
||||||
cwd="$(pwd -P)"
|
cwd="$(pwd -P)"
|
||||||
|
|
||||||
vale_args=()
|
vale_args=()
|
||||||
file_args=()
|
path_args=()
|
||||||
config_next=false
|
config_next=false
|
||||||
|
config_given=false
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
if [[ "$config_next" == true ]]; then
|
if [[ "$config_next" == true ]]; then
|
||||||
config_next=false
|
config_next=false
|
||||||
@@ -43,19 +52,24 @@ for arg in "$@"; do
|
|||||||
--config)
|
--config)
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
config_next=true
|
config_next=true
|
||||||
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
--config=/*)
|
--config=/*)
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
--config=*)
|
--config=*)
|
||||||
vale_args+=("--config=$cwd/${arg#--config=}")
|
vale_args+=("--config=$cwd/${arg#--config=}")
|
||||||
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
# `-f` resolves relative paths against the caller's cwd, same as vale does.
|
# `-f`/`-d` resolve relative paths against the caller's cwd, same as vale does.
|
||||||
if [[ "$arg" != -* && -f "$arg" ]]; then
|
# A path that doesn't exist is left for vale to report on, exactly as bare
|
||||||
|
# vale would.
|
||||||
|
if [[ "$arg" != -* && ( -f "$arg" || -d "$arg" ) ]]; then
|
||||||
# An absolute path inside the caller's cwd is relativized so the report cites
|
# 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
|
# 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
|
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
|
||||||
@@ -63,54 +77,41 @@ for arg in "$@"; do
|
|||||||
# quoting it. Absolute paths outside the cwd have no relative form and keep
|
# quoting it. Absolute paths outside the cwd have no relative form and keep
|
||||||
# the scratch-path behaviour documented above.
|
# the scratch-path behaviour documented above.
|
||||||
if [[ "$arg" == "$cwd"/* ]]; then
|
if [[ "$arg" == "$cwd"/* ]]; then
|
||||||
file_args+=("${arg#"$cwd"/}")
|
path_args+=("${arg#"$cwd"/}")
|
||||||
else
|
else
|
||||||
file_args+=("$arg")
|
path_args+=("$arg")
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
vale_args+=("$arg")
|
vale_args+=("$arg")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ${#file_args[@]} -eq 0 ]]; then
|
if [[ "$config_given" == false ]]; then
|
||||||
|
vale_args+=(--config "$(cd "$(dirname "${BASH_SOURCE[0]}")/../assets/vale" && pwd)/.vale.ini")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#path_args[@]} -eq 0 ]]; then
|
||||||
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
||||||
# block waiting on a pipe that will never carry content.
|
# 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="$(realpath -m "$(mktemp -d)")"
|
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
|
||||||
trap 'rm -rf "$tmpdir"' EXIT
|
# is a GNU extension the BSD realpath on macOS doesn't have — and every dest
|
||||||
|
# below is a path that doesn't exist yet. python3 is already a hard dependency.
|
||||||
|
abspath() {
|
||||||
|
python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
# Mirror of the caller's cwd inside the scratch tree; relative file arguments
|
flatten() {
|
||||||
# are resolved from here.
|
python3 - "$1" "$2" <<'PYTHON'
|
||||||
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 re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
src, dest = sys.argv[1], sys.argv[2]
|
src, dest = sys.argv[1], sys.argv[2]
|
||||||
with open(src) as fh:
|
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
|
||||||
|
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
|
||||||
|
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
content = fh.read()
|
content = fh.read()
|
||||||
|
|
||||||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||||||
@@ -163,15 +164,60 @@ if fm_match:
|
|||||||
new_fm = fm[:start] + f'description: {flat_q}\n{pad}' + fm[end:]
|
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():]
|
content = fm_match.group(1) + new_fm + fm_match.group(3) + content[fm_match.end():]
|
||||||
|
|
||||||
with open(dest, 'w') as fh:
|
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
fh.write(content)
|
fh.write(content)
|
||||||
PYTHON
|
PYTHON
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpdir="$(cd "$(mktemp -d)" && pwd -P)"
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
|
||||||
|
# Mirror of the caller's cwd inside the scratch tree; relative path arguments
|
||||||
|
# are resolved from here.
|
||||||
|
mirror="$tmpdir$cwd"
|
||||||
|
mkdir -p "$mirror"
|
||||||
|
|
||||||
|
argv_paths=()
|
||||||
|
for arg in "${path_args[@]}"; do
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
argv_files+=("$dest")
|
dest="$tmpdir$arg"
|
||||||
else
|
else
|
||||||
argv_files+=("$arg")
|
dest="$mirror/$arg"
|
||||||
|
fi
|
||||||
|
dest="$(abspath "$dest")"
|
||||||
|
# A path 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")"
|
||||||
|
if [[ -d "$arg" ]]; then
|
||||||
|
# A directory is mirrored whole — vale applies its own format filtering to
|
||||||
|
# the tree, so any file dropped here would be silently unlinted — and then
|
||||||
|
# every markdown file in the copy is flattened in place. `.git` is pruned:
|
||||||
|
# vale never lints it and copying it can dwarf the rest of the tree.
|
||||||
|
mkdir -p "$dest"
|
||||||
|
while IFS= read -r -d '' rel; do
|
||||||
|
mkdir -p "$dest/$(dirname "$rel")"
|
||||||
|
cp "$arg/$rel" "$dest/$rel"
|
||||||
|
done < <(cd "$arg" && find . -name .git -prune -o -type f -print0)
|
||||||
|
while IFS= read -r -d '' md; do
|
||||||
|
flatten "$md" "$md"
|
||||||
|
done < <(find "$dest" -type f -name '*.md' -print0)
|
||||||
|
else
|
||||||
|
flatten "$arg" "$dest"
|
||||||
|
fi
|
||||||
|
if [[ "$arg" == /* ]]; then
|
||||||
|
argv_paths+=("$dest")
|
||||||
|
else
|
||||||
|
argv_paths+=("$arg")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
cd "$mirror"
|
cd "$mirror"
|
||||||
vale "${vale_args[@]}" "${argv_files[@]}"
|
vale "${vale_args[@]}" "${argv_paths[@]}"
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"name": "lint",
|
"name": "lint",
|
||||||
"version": "1.1.3"
|
"version": "1.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ Lints the given file(s)/glob against the styles configured in `.vale.ini`.
|
|||||||
| `vale sync` | Downloads and installs packages/styles declared in `.vale.ini`. Run after install and whenever `Packages` changes. |
|
| `vale sync` | Downloads and installs packages/styles declared in `.vale.ini`. Run after install and whenever `Packages` changes. |
|
||||||
| `vale ls-config` | Prints the currently active, fully-resolved configuration as JSON. Useful for debugging what settings actually apply to a file. |
|
| `vale ls-config` | Prints the currently active, fully-resolved configuration as JSON. Useful for debugging what settings actually apply to a file. |
|
||||||
| `--output=<style>` | Sets the output format/template: `line`, `JSON`, `CLI` (default), or a custom template. |
|
| `--output=<style>` | Sets the output format/template: `line`, `JSON`, `CLI` (default), or a custom template. |
|
||||||
| `--no-exit` | Suppresses the non-zero exit code Vale normally returns when alerts are found — useful in CI pipelines that shouldn't hard-fail on lint output. |
|
| `--no-exit` | Suppresses the non-zero exit code Vale normally returns when `error`-level alerts are found — useful in CI pipelines that shouldn't hard-fail on lint output. |
|
||||||
| `--ignore-syntax` | Treats input as plain, unformatted text, skipping syntax-aware parsing (Markdown/HTML/etc). |
|
| `--ignore-syntax` | Treats input as plain, unformatted text, skipping syntax-aware parsing (Markdown/HTML/etc). |
|
||||||
| `--minAlertLevel=<level>` | Overrides `MinAlertLevel` from the config for this run (`suggestion`, `warning`, `error`). |
|
| `--minAlertLevel=<level>` | Overrides `MinAlertLevel` from the config for this run (`suggestion`, `warning`, `error`). Filters what is displayed; does not affect the exit code. |
|
||||||
| `--version` | Prints the Vale binary version. |
|
| `--version` | Prints the Vale binary version. |
|
||||||
|
|
||||||
## Exit Codes
|
## Exit Codes
|
||||||
|
|
||||||
By default, `vale` exits non-zero when it finds any alert at or above `MinAlertLevel` — this is what makes it usable as a CI gate. Pass `--no-exit` to always exit `0` regardless of findings.
|
By default, `vale` exits non-zero only when it finds at least one `error`-level alert — this is what makes it usable as a CI gate. `warning` and `suggestion` alerts are printed but still exit `0`, so a rule that must gate CI or a commit hook has to be `level: error`. `MinAlertLevel` and `--minAlertLevel` filter which alerts are displayed and never affect the exit code. Pass `--no-exit` to always exit `0` regardless of findings.
|
||||||
|
|||||||
@@ -53,4 +53,6 @@ If a file's syntax-aware parsing produces noisy/incorrect results (e.g. an unsup
|
|||||||
|
|
||||||
## 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.
|
If a CI job fails solely because Vale returns a non-zero exit code on `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. Raising `MinAlertLevel` is not an alternative: it only filters which alerts print, so an `error`-level alert still exits non-zero.
|
||||||
|
|
||||||
|
The mirror-image failure is a Vale gate that never fails. Only `error`-level alerts drive the exit code, so a `warning`- or `suggestion`-level rule prints its alert and still exits `0` — invisible in any CI stage that hides passing output. If a rule must block, give it `level: error`.
|
||||||
|
|||||||
@@ -18,5 +18,5 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"skills/"
|
"skills/"
|
||||||
],
|
],
|
||||||
"version": "1.1.3"
|
"version": "1.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,19 +34,29 @@ fi
|
|||||||
# instead of hand-maintaining a parallel list — the manifest is the single
|
# instead of hand-maintaining a parallel list — the manifest is the single
|
||||||
# source of truth for what external consumers actually pull at a pinned rev,
|
# source of truth for what external consumers actually pull at a pinned rev,
|
||||||
# so a hook added/removed/renamed there can't silently drift out of sync here.
|
# so a hook added/removed/renamed there can't silently drift out of sync here.
|
||||||
# Each entry is "<script> [--config <path>] [...]"; the script itself and the
|
# Everything is derived from tokens[0], the hook's script: pre-commit prefixes
|
||||||
# directory holding any --config target (vale-wrap.sh needs its .vale.ini's
|
# only entry[0] with the hook-repo clone path, so any later token that looks
|
||||||
# sibling styles/ tree, not just the ini file) are release-relevant.
|
# like a path resolves against the *consuming* repo and can never name a file
|
||||||
|
# this repo ships. A hook's bundled data therefore has to be self-located
|
||||||
|
# relative to the script — vale-wrap.sh reads its own
|
||||||
|
# <script-dir>/../assets/vale/.vale.ini plus the sibling styles/ tree — which
|
||||||
|
# makes <script-dir>/../assets release-relevant alongside the script itself.
|
||||||
|
# The ../ is normalised by stripping a path component rather than with
|
||||||
|
# `realpath -m`, which is a GNU-only extension. Two guards keep the derivation
|
||||||
|
# from inventing paths: a bundle root of "." is skipped, because a script in a
|
||||||
|
# top-level directory (scripts/skill-size-check.sh) would derive the repo's own
|
||||||
|
# shared assets/, which no hook owns and whose churn must not demand a release;
|
||||||
|
# and the directory is added only when it exists, since a hook that bundles
|
||||||
|
# nothing must not contribute a pathspec matching nothing.
|
||||||
RELEASE_PATHS=("$HOOKS_MANIFEST")
|
RELEASE_PATHS=("$HOOKS_MANIFEST")
|
||||||
while IFS= read -r entry; do
|
while IFS= read -r entry; do
|
||||||
read -ra tokens <<< "$entry"
|
read -ra tokens <<< "$entry"
|
||||||
[[ ${#tokens[@]} -eq 0 ]] && continue
|
[[ ${#tokens[@]} -eq 0 ]] && continue
|
||||||
RELEASE_PATHS+=("${tokens[0]}")
|
RELEASE_PATHS+=("${tokens[0]}")
|
||||||
for ((i = 1; i < ${#tokens[@]}; i++)); do
|
bundle_root="$(dirname "$(dirname "${tokens[0]}")")"
|
||||||
if [[ "${tokens[i]}" == "--config" && -n "${tokens[i + 1]:-}" ]]; then
|
if [[ "$bundle_root" != "." && -d "$bundle_root/assets" ]]; then
|
||||||
RELEASE_PATHS+=("$(dirname "${tokens[i + 1]}")")
|
RELEASE_PATHS+=("$bundle_root/assets")
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
done < <(sed -n 's/^[[:space:]]*entry:[[:space:]]*//p' "$HOOKS_MANIFEST")
|
done < <(sed -n 's/^[[:space:]]*entry:[[:space:]]*//p' "$HOOKS_MANIFEST")
|
||||||
|
|
||||||
# Only vX.Y.Z release tags count as a baseline — an incidental checkpoint or
|
# Only vX.Y.Z release tags count as a baseline — an incidental checkpoint or
|
||||||
|
|||||||
@@ -17,10 +17,22 @@ err() { echo " FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
|||||||
SKILL_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/skill-audit"
|
SKILL_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/skill-audit"
|
||||||
AGENT_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/agent-audit"
|
AGENT_AUDIT="$REPO_ROOT/plugins/kyberforge/skills/agent-audit"
|
||||||
|
|
||||||
if [[ ! -d "$SKILL_AUDIT" || ! -d "$AGENT_AUDIT" ]]; then
|
if [[ ! -d "$SKILL_AUDIT" && ! -d "$AGENT_AUDIT" ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Exactly one present is drift, not absence: the missing copy can't be in sync
|
||||||
|
# with the surviving one, and treating it as a no-op is how a deleted or
|
||||||
|
# renamed copy would slip through silently.
|
||||||
|
if [[ ! -d "$SKILL_AUDIT" ]]; then
|
||||||
|
echo "Vale style sync check failed: $AGENT_AUDIT exists but $SKILL_AUDIT does not — run scripts/sync-vale-styles.sh to regenerate skill-audit's copy." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$AGENT_AUDIT" ]]; then
|
||||||
|
echo "Vale style sync check failed: $SKILL_AUDIT exists but $AGENT_AUDIT does not — agent-audit holds the canonical copy, so restore it before syncing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! diff -q "$SKILL_AUDIT/scripts/vale-wrap.sh" "$AGENT_AUDIT/scripts/vale-wrap.sh" >/dev/null 2>&1; then
|
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"
|
err "scripts/vale-wrap.sh differs between skill-audit and agent-audit"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
# Run all test-*.sh files in the repo (including plugins) and the bats suite.
|
# Run all test-*.sh files in the repo (including plugins) and the bats suite.
|
||||||
# Usage: bash tests/run-tests.sh [--bats-only]
|
# Usage: bash tests/run-tests.sh [--bats-only]
|
||||||
#
|
#
|
||||||
|
# A script exiting 77 (the automake convention) is reported as SKIPPED, not
|
||||||
|
# passed — a suite that can't run for lack of a binary must not read as green.
|
||||||
|
#
|
||||||
# TEST_DIR — override root to search for test-*.sh (default: REPO_ROOT); used by tests.
|
# TEST_DIR — override root to search for test-*.sh (default: REPO_ROOT); used by tests.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -13,7 +16,9 @@ BATS_ONLY=false
|
|||||||
SEARCH_ROOT="${TEST_DIR:-$REPO_ROOT}"
|
SEARCH_ROOT="${TEST_DIR:-$REPO_ROOT}"
|
||||||
|
|
||||||
FAILED=()
|
FAILED=()
|
||||||
|
SKIPPED=()
|
||||||
PASSED=0
|
PASSED=0
|
||||||
|
SKIP_EXIT=77
|
||||||
|
|
||||||
run_bats() {
|
run_bats() {
|
||||||
if [[ -x "$BATS" ]]; then
|
if [[ -x "$BATS" ]]; then
|
||||||
@@ -40,15 +45,25 @@ mapfile -t SCRIPTS < <(
|
|||||||
for script in "${SCRIPTS[@]}"; do
|
for script in "${SCRIPTS[@]}"; do
|
||||||
rel="${script#"$SEARCH_ROOT/"}"
|
rel="${script#"$SEARCH_ROOT/"}"
|
||||||
echo "=== $rel ==="
|
echo "=== $rel ==="
|
||||||
if bash "$script"; then
|
rc=0
|
||||||
|
bash "$script" || rc=$?
|
||||||
|
if [[ $rc -eq 0 ]]; then
|
||||||
PASSED=$((PASSED + 1))
|
PASSED=$((PASSED + 1))
|
||||||
|
elif [[ $rc -eq $SKIP_EXIT ]]; then
|
||||||
|
SKIPPED+=("$rel")
|
||||||
else
|
else
|
||||||
FAILED+=("$rel")
|
FAILED+=("$rel")
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "=== Summary: $PASSED passed, ${#FAILED[@]} failed ==="
|
echo "=== Summary: $PASSED passed, ${#SKIPPED[@]} skipped, ${#FAILED[@]} failed ==="
|
||||||
|
if [[ ${#SKIPPED[@]} -gt 0 ]]; then
|
||||||
|
echo "Skipped scripts:"
|
||||||
|
for s in "${SKIPPED[@]}"; do
|
||||||
|
echo " $s"
|
||||||
|
done
|
||||||
|
fi
|
||||||
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||||
echo "Failed scripts:"
|
echo "Failed scripts:"
|
||||||
for s in "${FAILED[@]}"; do
|
for s in "${FAILED[@]}"; do
|
||||||
|
|||||||
@@ -9,32 +9,45 @@ FAIL=0
|
|||||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
# Helper: write a minimal .pre-commit-hooks.yaml exposing one script entry and
|
# Both entry shapes the real .pre-commit-hooks.yaml ships: a bare script with no
|
||||||
# one --config-bearing entry, so RELEASE_PATHS (derived from the manifest, not
|
# bundled data, and a bare script whose sibling assets/ tree it self-locates at
|
||||||
# hand-maintained) has both shapes to parse.
|
# runtime. Neither carries arguments — pre-commit only rewrites entry[0] to the
|
||||||
|
# hook-repo clone path, so an argument path would resolve against the consuming
|
||||||
|
# repo. RELEASE_PATHS is derived from the manifest rather than hand-maintained,
|
||||||
|
# so it has to cope with both.
|
||||||
|
HOOK_DIR="plugins/demo/skills/demo-audit"
|
||||||
|
|
||||||
write_manifest() {
|
write_manifest() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
mkdir -p "$dir/vale"
|
cat > "$dir/.pre-commit-hooks.yaml" <<EOF
|
||||||
cat > "$dir/.pre-commit-hooks.yaml" <<'EOF'
|
|
||||||
- id: fake-size-check
|
- id: fake-size-check
|
||||||
entry: scripts/skill-size-check.sh
|
entry: scripts/skill-size-check.sh
|
||||||
language: script
|
language: script
|
||||||
- id: fake-vale-check
|
- id: fake-vale-check
|
||||||
entry: scripts/vale-wrap.sh --config vale/.vale.ini
|
entry: $HOOK_DIR/scripts/vale-wrap.sh
|
||||||
language: script
|
language: script
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: a fixture repo with a manifest and its two referenced release-relevant
|
# Helper: writes the files both manifest entries expose — the two hook scripts
|
||||||
# paths, committed and tagged v1.0.0.
|
# plus the bundled Vale config and style rule the second one self-locates.
|
||||||
|
write_release_paths() {
|
||||||
|
local dir="$1"
|
||||||
|
mkdir -p "$dir/scripts" "$dir/$HOOK_DIR/scripts" "$dir/$HOOK_DIR/assets/vale/styles/Kyberforge"
|
||||||
|
echo "v1" > "$dir/scripts/skill-size-check.sh"
|
||||||
|
echo "v1" > "$dir/$HOOK_DIR/scripts/vale-wrap.sh"
|
||||||
|
echo "cfg" > "$dir/$HOOK_DIR/assets/vale/.vale.ini"
|
||||||
|
echo "rule: v1" > "$dir/$HOOK_DIR/assets/vale/styles/Kyberforge/DemoRule.yml"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: a fixture repo with a manifest and every release-relevant path it
|
||||||
|
# exposes, committed and tagged v1.0.0.
|
||||||
make_tagged_fixture() {
|
make_tagged_fixture() {
|
||||||
local dir
|
local dir
|
||||||
dir="$(mktemp -d)"
|
dir="$(mktemp -d)"
|
||||||
(cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t)
|
(cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t)
|
||||||
write_manifest "$dir"
|
write_manifest "$dir"
|
||||||
mkdir -p "$dir/scripts"
|
write_release_paths "$dir"
|
||||||
echo "v1" > "$dir/scripts/skill-size-check.sh"
|
|
||||||
echo "cfg" > "$dir/vale/.vale.ini"
|
|
||||||
(cd "$dir" && git add -A && git commit -q -m "initial" && git tag v1.0.0)
|
(cd "$dir" && git add -A && git commit -q -m "initial" && git tag v1.0.0)
|
||||||
echo "$dir"
|
echo "$dir"
|
||||||
}
|
}
|
||||||
@@ -65,9 +78,7 @@ echo "--- exits 1 when targeting main and no tag exists ---"
|
|||||||
FIXTURE2="$(mktemp -d)"; track "$FIXTURE2"
|
FIXTURE2="$(mktemp -d)"; track "$FIXTURE2"
|
||||||
(cd "$FIXTURE2" && git init -q && git config user.email t@t.t && git config user.name t)
|
(cd "$FIXTURE2" && git init -q && git config user.email t@t.t && git config user.name t)
|
||||||
write_manifest "$FIXTURE2"
|
write_manifest "$FIXTURE2"
|
||||||
mkdir -p "$FIXTURE2/scripts"
|
write_release_paths "$FIXTURE2"
|
||||||
echo "v1" > "$FIXTURE2/scripts/skill-size-check.sh"
|
|
||||||
echo "cfg" > "$FIXTURE2/vale/.vale.ini"
|
|
||||||
(cd "$FIXTURE2" && git add -A && git commit -q -m "initial")
|
(cd "$FIXTURE2" && git add -A && git commit -q -m "initial")
|
||||||
if run_check "$FIXTURE2" "refs/heads/main" > /dev/null; then
|
if run_check "$FIXTURE2" "refs/heads/main" > /dev/null; then
|
||||||
fail "exited 0 when targeting main with no tag — expected exit 1"
|
fail "exited 0 when targeting main with no tag — expected exit 1"
|
||||||
@@ -116,10 +127,10 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "--- exits 1 when a release-relevant path was deleted since the tag, not just modified ---"
|
echo "--- exits 1 when a release-relevant path was deleted since the tag, not just modified ---"
|
||||||
FIXTURE6="$(make_tagged_fixture)"; track "$FIXTURE6"
|
FIXTURE6="$(make_tagged_fixture)"; track "$FIXTURE6"
|
||||||
rm -rf "$FIXTURE6/vale"
|
rm -f "$FIXTURE6/$HOOK_DIR/assets/vale/.vale.ini"
|
||||||
(cd "$FIXTURE6" && git add -A && git commit -q -m "delete the vale config dir")
|
(cd "$FIXTURE6" && git add -A && git commit -q -m "delete the bundled vale config")
|
||||||
OUT6=$(run_check "$FIXTURE6" "refs/heads/main" || true)
|
OUT6=$(run_check "$FIXTURE6" "refs/heads/main" || true)
|
||||||
if echo "$OUT6" | grep -q "vale/.vale.ini"; then
|
if echo "$OUT6" | grep -q "assets/vale/.vale.ini"; then
|
||||||
pass "flags a deleted release-relevant path instead of silently dropping it from the diff"
|
pass "flags a deleted release-relevant path instead of silently dropping it from the diff"
|
||||||
else
|
else
|
||||||
fail "did not flag deletion of a release-relevant path since the tag"
|
fail "did not flag deletion of a release-relevant path since the tag"
|
||||||
@@ -166,6 +177,40 @@ else
|
|||||||
fail "flagged a file that no .pre-commit-hooks.yaml entry actually exposes"
|
fail "flagged a file that no .pre-commit-hooks.yaml entry actually exposes"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 10. A change confined to a hook's bundled styles/ tree is release-relevant ---
|
||||||
|
# The manifest entry names only the wrapper script; the Vale rules it enforces
|
||||||
|
# live in the sibling assets/ tree it self-locates at runtime. If that tree is
|
||||||
|
# not covered, editing a rule and landing it on main demands no new tag, and a
|
||||||
|
# consumer pinned to the old rev keeps the stale rules forever.
|
||||||
|
echo ""
|
||||||
|
echo "--- exits 1 when only a bundled Vale style rule changed since the tag ---"
|
||||||
|
FIXTURE10="$(make_tagged_fixture)"; track "$FIXTURE10"
|
||||||
|
echo "rule: v2" > "$FIXTURE10/$HOOK_DIR/assets/vale/styles/Kyberforge/DemoRule.yml"
|
||||||
|
(cd "$FIXTURE10" && git add -A && git commit -q -m "tighten a vale rule")
|
||||||
|
OUT10=$(run_check "$FIXTURE10" "refs/heads/main" || true)
|
||||||
|
if echo "$OUT10" | grep -q "assets/vale/styles/Kyberforge/DemoRule.yml"; then
|
||||||
|
pass "flags a change confined to a hook's bundled assets/vale/styles/ tree"
|
||||||
|
else
|
||||||
|
fail "a bundled Vale style rule changed since the tag without demanding a release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 11. The assets/ derivation must not invent a path for a bundle-less hook ---
|
||||||
|
# scripts/skill-size-check.sh has no sibling assets/ tree, so its derived
|
||||||
|
# candidate normalises to a bare top-level assets/ — a directory this repo does
|
||||||
|
# not ship. Adding it unconditionally would make any unrelated repo-root
|
||||||
|
# assets/ file falsely demand a release.
|
||||||
|
echo ""
|
||||||
|
echo "--- exits 0 when a top-level assets/ file changed but no hook bundles one ---"
|
||||||
|
FIXTURE11="$(make_tagged_fixture)"; track "$FIXTURE11"
|
||||||
|
mkdir -p "$FIXTURE11/assets"
|
||||||
|
echo "unrelated" > "$FIXTURE11/assets/logo.txt"
|
||||||
|
(cd "$FIXTURE11" && git add -A && git commit -q -m "add an unrelated top-level assets file")
|
||||||
|
if run_check "$FIXTURE11" "refs/heads/main" > /dev/null; then
|
||||||
|
pass "exits 0 for a top-level assets/ file that no manifest entry bundles"
|
||||||
|
else
|
||||||
|
fail "invented a bogus assets/ path for a hook script with no bundled tree"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[[ $FAIL -eq 0 ]]
|
[[ $FAIL -eq 0 ]]
|
||||||
|
|||||||
@@ -86,6 +86,27 @@ else
|
|||||||
fail "exited non-zero when skill-audit/agent-audit are simply absent"
|
fail "exited non-zero when skill-audit/agent-audit are simply absent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 6. Exits 1 when only one of the two copies is present ---
|
||||||
|
# The no-op guard used `||`, so a single missing copy also exited 0 — a deleted
|
||||||
|
# or renamed copy passed the sync check silently.
|
||||||
|
echo ""
|
||||||
|
echo "--- exits 1 when only one of the two copies is present ---"
|
||||||
|
FIXTURE6="$(make_fixture)"
|
||||||
|
FIXTURE7="$(make_fixture)"
|
||||||
|
trap 'rm -rf "$FIXTURE" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7"' EXIT
|
||||||
|
rm -rf "$FIXTURE6/plugins/kyberforge/skills/skill-audit"
|
||||||
|
rm -rf "$FIXTURE7/plugins/kyberforge/skills/agent-audit"
|
||||||
|
if bash "$SCRIPT" "$FIXTURE6" > /dev/null 2>&1; then
|
||||||
|
fail "exited 0 when only agent-audit is present — expected exit 1"
|
||||||
|
else
|
||||||
|
pass "exits non-zero when skill-audit's copy is missing but agent-audit's is present"
|
||||||
|
fi
|
||||||
|
if bash "$SCRIPT" "$FIXTURE7" > /dev/null 2>&1; then
|
||||||
|
fail "exited 0 when only skill-audit is present — expected exit 1"
|
||||||
|
else
|
||||||
|
pass "exits non-zero when agent-audit's canonical copy is missing but skill-audit's is present"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[[ $FAIL -eq 0 ]]
|
[[ $FAIL -eq 0 ]]
|
||||||
|
|||||||
127
tests/test-vale-hooks-consumer.sh
Executable file
127
tests/test-vale-hooks-consumer.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test for .pre-commit-hooks.yaml as an EXTERNAL hook repo — the
|
||||||
|
# contract ADR-0014 exists to provide, and the one thing running pre-commit
|
||||||
|
# inside this repo can never exercise: `repo: local` makes pre-commit's clone
|
||||||
|
# prefix equal to the consuming repo's root, so a hook entry that only works
|
||||||
|
# because those two coincide passes here and hard-fails everywhere else.
|
||||||
|
# (It did: every argument after entry[0] resolves against the CONSUMING repo,
|
||||||
|
# so a `--config plugins/.../.vale.ini` argument gave external consumers
|
||||||
|
# `E100 [--config] Runtime error ... does not exist`, exit 2, on both Vale hooks.)
|
||||||
|
#
|
||||||
|
# The hook repo is built from the WORKING TREE, not from HEAD, so an uncommitted
|
||||||
|
# change to the manifest or the wrapper is what gets tested.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
for bin in pre-commit vale git; do
|
||||||
|
if ! command -v "$bin" &>/dev/null; then
|
||||||
|
echo "SKIP: $bin is not installed — cannot stand up a consumer repo"
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
HOOK_REPO="$WORK/hookrepo"
|
||||||
|
CONSUMER="$WORK/consumer"
|
||||||
|
export PRE_COMMIT_HOME="$WORK/pc-home"
|
||||||
|
|
||||||
|
mkdir -p "$HOOK_REPO/plugins/kyberforge/skills" "$HOOK_REPO/scripts"
|
||||||
|
cp "$REPO_ROOT/.pre-commit-hooks.yaml" "$HOOK_REPO/"
|
||||||
|
cp "$REPO_ROOT/scripts/skill-size-check.sh" "$HOOK_REPO/scripts/"
|
||||||
|
for skill in skill-audit agent-audit; do
|
||||||
|
mkdir -p "$HOOK_REPO/plugins/kyberforge/skills/$skill"
|
||||||
|
cp -R "$REPO_ROOT/plugins/kyberforge/skills/$skill/scripts" \
|
||||||
|
"$REPO_ROOT/plugins/kyberforge/skills/$skill/assets" \
|
||||||
|
"$HOOK_REPO/plugins/kyberforge/skills/$skill/"
|
||||||
|
done
|
||||||
|
git -C "$HOOK_REPO" init -q
|
||||||
|
git -C "$HOOK_REPO" add -A
|
||||||
|
git -C "$HOOK_REPO" -c user.email=test@example.invalid -c user.name=test commit -qm "hook repo"
|
||||||
|
HOOK_REV="$(git -C "$HOOK_REPO" rev-parse HEAD)"
|
||||||
|
|
||||||
|
# The two Vale hooks scope by filename, so the consumer needs one file of each
|
||||||
|
# shape: a hook with nothing to match reports `Skipped` and proves nothing.
|
||||||
|
mkdir -p "$CONSUMER/skills/demo" "$CONSUMER/agents"
|
||||||
|
git -C "$CONSUMER" init -q
|
||||||
|
cat > "$CONSUMER/.pre-commit-config.yaml" <<EOF
|
||||||
|
repos:
|
||||||
|
- repo: file://$HOOK_REPO
|
||||||
|
rev: $HOOK_REV
|
||||||
|
hooks:
|
||||||
|
- id: kyberforge-vale-audit-skill
|
||||||
|
- id: kyberforge-vale-audit-agent
|
||||||
|
EOF
|
||||||
|
|
||||||
|
write_fixtures() {
|
||||||
|
local body="$1"
|
||||||
|
cat > "$CONSUMER/skills/demo/SKILL.md" <<EOF
|
||||||
|
---
|
||||||
|
name: demo
|
||||||
|
description: >
|
||||||
|
Use when the caller wants a demonstration skill $body across two
|
||||||
|
physical lines of one folded block scalar.
|
||||||
|
---
|
||||||
|
|
||||||
|
Body.
|
||||||
|
EOF
|
||||||
|
cat > "$CONSUMER/agents/demo.md" <<EOF
|
||||||
|
---
|
||||||
|
name: demo
|
||||||
|
description: >
|
||||||
|
Use when the caller wants a demonstration agent $body across two
|
||||||
|
physical lines of one folded block scalar.
|
||||||
|
---
|
||||||
|
|
||||||
|
Body.
|
||||||
|
EOF
|
||||||
|
git -C "$CONSUMER" add -A
|
||||||
|
}
|
||||||
|
|
||||||
|
run_hooks() {
|
||||||
|
(cd "$CONSUMER" && pre-commit run --all-files 2>&1) || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1. Both hooks resolve their config and actually gate on a bad file ---
|
||||||
|
echo ""
|
||||||
|
echo "--- both Vale hooks run and fail a bad file in an external consumer repo ---"
|
||||||
|
write_fixtures "that helps with and utilize things"
|
||||||
|
OUT_BAD="$(run_hooks)"
|
||||||
|
if echo "$OUT_BAD" | grep -q "does not exist"; then
|
||||||
|
fail "hooks hard-errored on a path resolved against the consumer repo (E100) — the bug this test guards against"
|
||||||
|
echo "$OUT_BAD" | sed 's/^/ /'
|
||||||
|
elif echo "$OUT_BAD" | grep -q "Skipped"; then
|
||||||
|
fail "a hook matched no files, so it proved nothing"
|
||||||
|
echo "$OUT_BAD" | sed 's/^/ /'
|
||||||
|
elif [[ "$(echo "$OUT_BAD" | grep -c "VagueWording")" -ge 2 ]]; then
|
||||||
|
pass "both hooks flatten and flag the folded description in a consumer repo"
|
||||||
|
else
|
||||||
|
fail "hooks did not flag both fixtures"
|
||||||
|
echo "$OUT_BAD" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2. Clean files pass — the hooks gate, they don't just always fail ---
|
||||||
|
echo ""
|
||||||
|
echo "--- both Vale hooks pass clean files in an external consumer repo ---"
|
||||||
|
write_fixtures "of the packaged hook contract"
|
||||||
|
set +e
|
||||||
|
(cd "$CONSUMER" && pre-commit run --all-files > "$WORK/clean.log" 2>&1)
|
||||||
|
RC_CLEAN=$?
|
||||||
|
set -e
|
||||||
|
if [[ $RC_CLEAN -eq 0 ]]; then
|
||||||
|
pass "both hooks exit 0 on clean files"
|
||||||
|
else
|
||||||
|
fail "hooks failed on clean files (rc=$RC_CLEAN)"
|
||||||
|
sed 's/^/ /' "$WORK/clean.log"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
|
[[ $FAIL -eq 0 ]]
|
||||||
@@ -19,8 +19,8 @@ pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|||||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
if ! command -v vale &>/dev/null; then
|
if ! command -v vale &>/dev/null; then
|
||||||
echo "vale is not installed — skipping (matches skill-audit/agent-audit's own fallback behavior)"
|
echo "SKIP: vale is not installed — skipping (matches skill-audit/agent-audit's own fallback behavior)"
|
||||||
exit 0
|
exit 77
|
||||||
fi
|
fi
|
||||||
|
|
||||||
make_fixture() {
|
make_fixture() {
|
||||||
@@ -345,6 +345,86 @@ else
|
|||||||
fail "wrapper altered output for a literal (|) block scalar description — should be left untouched"
|
fail "wrapper altered output for a literal (|) block scalar description — should be left untouched"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 12. With no --config at all, the wrapper falls back to its own sibling
|
||||||
|
# assets/vale/.vale.ini. `.pre-commit-hooks.yaml` relies on this: pre-commit
|
||||||
|
# prefixes only entry[0] with the hook-repo clone path, so a --config argument
|
||||||
|
# there resolves against the consuming repo and hard-errors (E100) for every
|
||||||
|
# external consumer.
|
||||||
|
echo ""
|
||||||
|
echo "--- defaults --config to the wrapper's own sibling assets/vale/.vale.ini ---"
|
||||||
|
FIXTURE12="$(make_fixture 2)"
|
||||||
|
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12"' EXIT
|
||||||
|
OUT12=$(run_wrap "$FIXTURE12" plugins/testplugin/skills/zzzskill/SKILL.md)
|
||||||
|
if echo "$OUT12" | grep -q "VagueWording"; then
|
||||||
|
pass "a --config-less invocation uses the wrapper's bundled config"
|
||||||
|
else
|
||||||
|
fail "a --config-less invocation found no config — external pre-commit consumers get E100, the bug this test guards against"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 13. No GNU-only `realpath -m`. macOS ships the BSD realpath, which has no
|
||||||
|
# -m (canonicalize-missing) — and every scratch destination is a path that does
|
||||||
|
# not exist yet, so a plain `realpath` exits 1 and set -e aborts the hook.
|
||||||
|
echo ""
|
||||||
|
echo "--- runs with a BSD realpath that has no -m option ---"
|
||||||
|
STUB13="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13"' EXIT
|
||||||
|
REAL_REALPATH="$(command -v realpath || echo /bin/false)"
|
||||||
|
cat > "$STUB13/realpath" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
for a in "\$@"; do
|
||||||
|
case "\$a" in
|
||||||
|
-m|--canonicalize-missing)
|
||||||
|
echo "realpath: illegal option -- m" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
exec "$REAL_REALPATH" "\$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$STUB13/realpath"
|
||||||
|
OUT13=$(cd "$FIXTURE12" && PATH="$STUB13:$PATH" bash "$SCRIPT" --config "$VALE_CONFIG" \
|
||||||
|
plugins/testplugin/skills/zzzskill/SKILL.md 2>&1 || true)
|
||||||
|
if echo "$OUT13" | grep -q "illegal option"; then
|
||||||
|
fail "invoked realpath -m — fails on macOS's BSD realpath, the bug this test guards against"
|
||||||
|
elif echo "$OUT13" | grep -q "VagueWording"; then
|
||||||
|
pass "flattens and flags with no GNU realpath available"
|
||||||
|
else
|
||||||
|
fail "produced no alert under a BSD-style realpath: $OUT13"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 14. A directory argument is walked and its files flattened. The classifier
|
||||||
|
# used to accept only regular files, so a directory fell through to the vale
|
||||||
|
# flag list, left the file list empty, and exec'd bare vale — silently skipping
|
||||||
|
# the flattening. `lint`'s vale-run skill documents `vale <path-or-glob>` as
|
||||||
|
# normal usage, so this is a reachable path.
|
||||||
|
echo ""
|
||||||
|
echo "--- flattens files reached through a directory argument ---"
|
||||||
|
FIXTURE14="$(make_fixture 2)"
|
||||||
|
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" "$FIXTURE14"' EXIT
|
||||||
|
WRAPPED_DIR=$(run_wrap "$FIXTURE14" --config "$VALE_CONFIG" plugins)
|
||||||
|
BARE_DIR=$(cd "$FIXTURE14" && vale --config "$VALE_CONFIG" plugins 2>&1 || true)
|
||||||
|
if ! echo "$WRAPPED_DIR" | grep -q "VagueWording"; then
|
||||||
|
fail "a directory argument produced no alert — flattening was silently skipped, the bug this test guards against"
|
||||||
|
elif echo "$BARE_DIR" | grep -q "VagueWording"; then
|
||||||
|
fail "bare vale already flags this fixture, so the test can't detect a silently-skipped flattening"
|
||||||
|
else
|
||||||
|
pass "a directory argument is walked and its files flattened"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 15. Directory walking must survive paths with spaces ---
|
||||||
|
echo ""
|
||||||
|
echo "--- walks a directory containing a path with spaces ---"
|
||||||
|
SPACED15="$FIXTURE14/plugins/testplugin/skills/zzz skill"
|
||||||
|
mkdir -p "$SPACED15"
|
||||||
|
cp "$FIXTURE14/plugins/testplugin/skills/zzzskill/SKILL.md" "$SPACED15/SKILL.md"
|
||||||
|
rm -rf "$FIXTURE14/plugins/testplugin/skills/zzzskill"
|
||||||
|
OUT15=$(run_wrap "$FIXTURE14" --config "$VALE_CONFIG" plugins/testplugin/skills)
|
||||||
|
if echo "$OUT15" | grep -q "zzz skill" && echo "$OUT15" | grep -q "VagueWording"; then
|
||||||
|
pass "a file under a directory whose name contains a space is walked and flattened"
|
||||||
|
else
|
||||||
|
fail "a path with a space was dropped from the directory walk"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[[ $FAIL -eq 0 ]]
|
[[ $FAIL -eq 0 ]]
|
||||||
|
|||||||
Reference in New Issue
Block a user