feat(lint): wire Vale as deterministic prefilter for skill-audit/agent-audit #85

Merged
Defame1297 merged 45 commits from feat/84-vale-audit-prefilter into main 2026-08-10 16:46:59 +00:00
Collaborator

Summary

Implements issue #84: moves pattern-matchable qualitative checks in skill-audit/agent-audit (description opener phrasing, vague-capability wording, padding phrases, Copilot's "Use proactively" check) from LLM re-derivation to a deterministic Vale pass, per the repo's "prefer deterministic code for repeatable tasks" governance principle.

  • Add repo-root .vale.ini plus a Kyberforge style and a KyberforgeCopilot style scoped to .agent.md files. Every rule is level: error — Vale's exit code keys on error alerts alone, so any other level prints an alert and still exits 0, which makes it invisible behind a passing pre-commit hook. No ignorable tier, matching shellcheck and the test suite.
  • Wire Vale into skill-audit/agent-audit Step 1 alongside the existing validate.sh structural checks. Every Vale alert is a FAIL. A zero-file Vale run is treated as NOT RUN, not as clean.
  • .vale.ini's section globs are path-agnostic ([**/SKILL.md], [**/agents/*.md], [**/*.agent.md]) and deliberately do no scoping — Vale's * crosses /, so plugins/*/-prefixed globs never scoped anything either. Scoping is the pre-commit hook's files: regex.
  • Add a lint plugin (lint-runner agent, vale-config/vale-run skills, bundled Vale research docs) and register it in both marketplace manifests
  • Wire the same Vale prefilter as a commit-stage pre-commit hook, plus a sibling skill-size-check hook enforcing the 500-line SKILL.md ceiling
  • scripts/vale-wrap.sh works around a confirmed Vale 3.15.2 bug: text.frontmatter.description silently stops matching once the description is a multi-line YAML block scalar, which is how most skills here are written
  • The Vale config/styles/wrapper ship inside the kyberforge plugin itself (round 4), so the prefilter also works for repos that install kyberforge@holocron as an external plugin, not just this repo

Closes #84

Rebased onto main after #86 landed, so SHAs cited in earlier review comments no longer exist on the branch. Subjects are unchanged, so the mapping is 1:1 by commit message:

Old New Old New
0bbb965 bbb0dcd 4d6f313 57bdfa9
55dc065 792d3e1 0c0d51f 59ad2a3
6c0afb7 210b192 cb2257d 8b00728
2eb13f7 3324a73 e4abe23 d1afdbe
28058d3 544392b 88888c4 5e22672
7118ace 8d56290 75f65d8 cbc33d9
e1a5403 f326df4

Review history

Seven rounds, all resolved:

  1. #1291 → #1294 — multi-line folded descriptions silently defeated the frontmatter scope; pre-commit glob crossed directory boundaries
  2. #1299 → #1309 — escaping bugs in the wrapper, caveman regression, token-list noise, cwd resolution
  3. #1316 → #1327 — the hook did not actually gate; vale-run documented Vale's exit-code model backwards; a zero-file run read as clean
  4. #1339 → #1346 — made the prefilter portable to external plugin installs; external-consumer hard-fail, GNU-only realpath -m, directory args skipping flattening
  5. #1355 — one root cause (Vale matches the parsed YAML value back against the source text) explained four separate flattening bugs at once; fixed together
  6. #1364 → #1368 — 12 findings cross-checked against this thread before any code was written; a pre-merge pass added a bash-3.2 array-expansion sweep and efficiency/reuse cleanups
  7. #1373 — independent post-review pass: fixed a bash-3.2 declare -A regression introduced by round 6's own caching refactor (the file wasn't in round 6's bash-3.2 scan list), a version-bump gap on the gitea plugin (the same bug class was caught 4× elsewhere in this PR but missed here), and a silent-success gap when check-vale-style-sync.sh is given a bad path argument

Styles portability (moving styles//.vale.ini out of the repo root so the prefilter travels to external installs) — resolved in round 4 (#1339, commit 1164f3a) and formalized in ADR-0014, which supersedes ADR-0013's deferred-portability consequence.

Test plan

  • bash tests/run-tests.sh — 12 scripts, 0 failed (vale-wrap.sh sub-suite: 39 cases; check-vale-style-sync.sh sub-suite: 21 cases)
  • pre-commit run --all-files for vale-audit-prefilter, skill-size-check, shellcheck — all pass
  • scripts/check-manifests.sh and claude plugin validate --strict — clean on every plugin
  • New path-resolution tests checked for non-vacuity: they fail against the previous script, pass against the current one

🤖 Generated with Claude Code

https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn

## Summary Implements issue #84: moves pattern-matchable qualitative checks in `skill-audit`/`agent-audit` (description opener phrasing, vague-capability wording, padding phrases, Copilot's "Use proactively" check) from LLM re-derivation to a deterministic Vale pass, per the repo's "prefer deterministic code for repeatable tasks" governance principle. - Add repo-root `.vale.ini` plus a `Kyberforge` style and a `KyberforgeCopilot` style scoped to `.agent.md` files. **Every rule is `level: error`** — Vale's exit code keys on `error` alerts alone, so any other level prints an alert and still exits 0, which makes it invisible behind a passing pre-commit hook. No ignorable tier, matching shellcheck and the test suite. - Wire Vale into `skill-audit`/`agent-audit` Step 1 alongside the existing `validate.sh` structural checks. Every Vale alert is a FAIL. A zero-file Vale run is treated as NOT RUN, not as clean. - `.vale.ini`'s section globs are path-agnostic (`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]`) and deliberately do no scoping — Vale's `*` crosses `/`, so `plugins/*/`-prefixed globs never scoped anything either. Scoping is the pre-commit hook's `files:` regex. - Add a `lint` plugin (`lint-runner` agent, `vale-config`/`vale-run` skills, bundled Vale research docs) and register it in both marketplace manifests - Wire the same Vale prefilter as a commit-stage `pre-commit` hook, plus a sibling `skill-size-check` hook enforcing the 500-line `SKILL.md` ceiling - `scripts/vale-wrap.sh` works around a confirmed Vale 3.15.2 bug: `text.frontmatter.description` silently stops matching once the description is a multi-line YAML block scalar, which is how most skills here are written - The Vale config/styles/wrapper ship inside the `kyberforge` plugin itself (round 4), so the prefilter also works for repos that install `kyberforge@holocron` as an external plugin, not just this repo Closes #84 ## ⚠️ This branch was rebased — old commit links are dead Rebased onto `main` after #86 landed, so SHAs cited in earlier review comments no longer exist on the branch. Subjects are unchanged, so the mapping is 1:1 by commit message: | Old | New | | Old | New | |---|---|---|---|---| | `0bbb965` | `bbb0dcd` | | `4d6f313` | `57bdfa9` | | `55dc065` | `792d3e1` | | `0c0d51f` | `59ad2a3` | | `6c0afb7` | `210b192` | | `cb2257d` | `8b00728` | | `2eb13f7` | `3324a73` | | `e4abe23` | `d1afdbe` | | `28058d3` | `544392b` | | `88888c4` | `5e22672` | | `7118ace` | `8d56290` | | `75f65d8` | `cbc33d9` | | `e1a5403` | `f326df4` | | | | ## Review history Seven rounds, all resolved: 1. [#1291](#issuecomment-1291) → [#1294](#issuecomment-1294) — multi-line folded descriptions silently defeated the frontmatter scope; pre-commit glob crossed directory boundaries 2. [#1299](#issuecomment-1299) → [#1309](#issuecomment-1309) — escaping bugs in the wrapper, caveman regression, token-list noise, cwd resolution 3. [#1316](#issuecomment-1316) → [#1327](#issuecomment-1327) — the hook did not actually gate; `vale-run` documented Vale's exit-code model backwards; a zero-file run read as clean 4. [#1339](#issuecomment-1339) → [#1346](#issuecomment-1346) — made the prefilter portable to external plugin installs; external-consumer hard-fail, GNU-only `realpath -m`, directory args skipping flattening 5. [#1355](#issuecomment-1355) — one root cause (Vale matches the parsed YAML value back against the source text) explained four separate flattening bugs at once; fixed together 6. [#1364](#issuecomment-1364) → [#1368](#issuecomment-1368) — 12 findings cross-checked against this thread before any code was written; a pre-merge pass added a bash-3.2 array-expansion sweep and efficiency/reuse cleanups 7. [#1373](#issuecomment-1373) — independent post-review pass: fixed a bash-3.2 `declare -A` regression introduced by round 6's own caching refactor (the file wasn't in round 6's bash-3.2 scan list), a version-bump gap on the `gitea` plugin (the same bug class was caught 4× elsewhere in this PR but missed here), and a silent-success gap when `check-vale-style-sync.sh` is given a bad path argument Styles portability (moving `styles/`/`.vale.ini` out of the repo root so the prefilter travels to external installs) — resolved in round 4 ([#1339](#issuecomment-1339), commit `1164f3a`) and formalized in ADR-0014, which supersedes ADR-0013's deferred-portability consequence. ## Test plan - [x] `bash tests/run-tests.sh` — 12 scripts, 0 failed (`vale-wrap.sh` sub-suite: 39 cases; `check-vale-style-sync.sh` sub-suite: 21 cases) - [x] `pre-commit run --all-files` for `vale-audit-prefilter`, `skill-size-check`, `shellcheck` — all pass - [x] `scripts/check-manifests.sh` and `claude plugin validate --strict` — clean on every plugin - [x] New path-resolution tests checked for non-vacuity: they fail against the previous script, pass against the current one 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Claude added 8 commits 2026-07-23 20:54:04 +00:00
Prep work for issue #84 - gathers Vale (vale.sh) config, styles/rules,
CLI, installation, and troubleshooting reference material into
plugins/kyberforge/docs/research/docs/vale/ alongside the existing
research topics.

Refs #84
Covers Vale install and .vale.ini setup — StylesPath, built-in/
third-party/custom styles, BasedOnStyles activation. Setup half of
Vale support; vale-run (running/interpreting) is a separate skill.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Covers invoking the vale CLI and interpreting its output — output
formats, severity filtering, exit-code handling, and false-positive
triage — for an already-configured project.
Report-only agent that composes vale-config/vale-run to run a lint
sweep over a scope and return normalized findings — no Edit tool, it
flags issues rather than fixing them. Also lands the plugin manifest
scaffold (plugin.json, .claude-plugin/plugin.json) that the earlier
vale-config/vale-run skill commits assumed but didn't carry, bumped
to 1.1.0 for the new agent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Merge duplicate gotcha in vale-config (Packages vs BasedOnStyles was
stated twice) and align vale-run's category field with vale-config's
(lint, not linting) so sibling skills in the plugin agree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Adds the lint plugin entry to both marketplace manifests and records
the resolved scope/structure decisions from grilling in CONTEXT.md:
standalone repo-agnostic plugin, split vale-config/vale-run skills,
report-only lint-runner agent, audit-pipeline wiring deferred.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Adds repo-root .vale.ini plus a custom Kyberforge style (description-opener,
vague-wording, and generic reference-pointer padding rules) and a
KyberforgeCopilot style scoped to .agent.md files (Use proactively check).
skill-audit and agent-audit Step 1 now run vale against the specific file(s)
being audited and defer the corresponding Description/Patterns/Body checks
to its output instead of re-deriving them by LLM judgment, per the split
proposed in issue #84.

Closes #84

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Adds vale-audit-prefilter as a local pre-commit hook scoped to skill/agent
markdown files, matching the invocation pattern skill-audit/agent-audit
already use. Runs at commit-stage only since it's a fast deterministic
prefilter; push-stage already covers the full test suite and manifest checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Defame1297 requested changes 2026-07-23 21:41:30 +00:00
Dismissed
Defame1297 left a comment
Owner
  • Why split the check for copilot agents? Is it not relevant for other/claude agents?
  • Do we need all the prose about the vale kyberforge styles in the audit skills?
  • Since the kyberforge styles are relevant for the skills in the plugin, should the styles be included there to so the vale checkes can also be run in skills/agents created in other repos/environments?
  • The plugin jsons and lint runner agents are too vale-focussed, keep them generic for linters
  • Note to self: create issues for other linters currently in the repo
  • We need to check what we implemented and our setup/install/config/commithook with the created vale skills
  • we need to check our existing research for skills/agents (claude/copilot)/agents.mds/sourcing/provenance/etc to see if we can expand vsle coverage based on documentation/guidelines/best practices/examples
- Why split the check for copilot agents? Is it not relevant for other/claude agents? - Do we need all the prose about the vale kyberforge styles in the audit skills? - Since the kyberforge styles are relevant for the skills in the plugin, should the styles be included there to so the vale checkes can also be run in skills/agents created in other repos/environments? - The plugin jsons and lint runner agents are too vale-focussed, keep them generic for linters - Note to self: create issues for other linters currently in the repo - We need to check what we implemented and our setup/install/config/commithook with the created vale skills - we need to check our existing research for skills/agents (claude/copilot)/agents.mds/sourcing/provenance/etc to see if we can expand vsle coverage based on documentation/guidelines/best practices/examples
Author
Collaborator

Review (read-only — no edits made)

Verified locally by running vale directly (v3.15.2, installed at /root/.local/bin/vale) against the actual repo tree on this branch, rather than just reading the diff. Full test suite (bash tests/run-tests.sh) and scripts/check-manifests.sh both pass as claimed.

🔴 Blocking: multi-line folded descriptions silently defeat text.frontmatter.description scope

Kyberforge.DescriptionOpener and Kyberforge.VagueWording are scoped to text.frontmatter.description. Confirmed working when description: is a single physical line. But the moment the folded scalar (description: >) spans two or more physical lines — which is the style used by the large majority of skills in this repo, including skill-audit/agent-audit themselves — the scope silently stops matching. Reproduced directly:

---
name: zzzskill
description: >
  Use when the user wants to review a skill they wrote, says "audit this skill".
  This skill helps with reviewing things and utilize best practices.
---
$ vale --config .vale.ini SKILL.md
✔ 0 errors, 0 warnings and 0 suggestions in 1 file.

That's an obvious DescriptionOpener + two VagueWording hit, and Vale reports clean. A single-line version of the identical text triggers correctly. This means a full sweep of the repo's own 49 skill/agent files currently returns 0/0/0 — not because the repo is clean, but because the check that's supposed to cover most of the Description dimension never fires for anything using the folded style. Since skill-audit/agent-audit treat a clean Vale run as "no additional findings" for that dimension, this is a silent false negative, not a no-op — worse than not having the check at all, since it now reads as evidence of cleanliness.

Worth a real regression test asserting Vale fires against a fixture with a known-bad multi-line description — nothing in the added test coverage would have caught this, which is presumably how it shipped.

🟠 Gap: pre-commit hook glob crosses directory boundaries the audit-invocation path deliberately avoids

CONTEXT.md/the SKILL.md edits explicitly acknowledge that plugins/*/agents/*.md-style globs match nested paths like docs/research/examples/**/agents/*.md, and mitigate it by always scoping skill-audit/agent-audit's Step 1 vale invocation to a specific target file rather than a sweep. Confirmed that mitigation holds for the audit-skill path.

It does not hold for the new .pre-commit-config.yaml hook (vale-audit-prefilter), whose files regex (^plugins/.*/(skills/.*/SKILL\.md|agents/.*\.md)$) has the same cross-boundary behavior and is not manually scoped — pre-commit runs it automatically against whatever staged files match. Confirmed this matches (and would lint) e.g. plugins/kyberforge/docs/research/examples/skill-write/skill-creator/agents/*.md (upstream reference material, explicitly documented as "not shipped with the plugin") and plugins/kyberforge/skills/skill-author/assets/templates/SKILL.md (a placeholder template). Also confirmed Vale exits 1 on any error-level alert, which language: unsupported pre-commit hooks treat as hook failure.

Net effect: a future commit touching either of those paths — upstream example content or the FILL-IN template — that happens to contain house-style-violating phrasing (plausible for both, since neither is written to Kyberforge's own description conventions) will get blocked by a hook whose stated purpose is auditing Kyberforge-authored skills/agents, not arbitrary reference docs. Suggest tightening the files regex (e.g. excluding docs/research/ and assets/) to match the audit path's scoping intent.

🟡 Minor / worth confirming before merge

plugins/lint/agents/lint-runner.agent.md uses tools: ["execute", "read", "search"]. The two existing orchestrator agent pairs (git-orchestrate, gitea-orchestrate) only ever use execute/read/edit — search is a new tool identifier with no precedent elsewhere in the repo. Worth double-checking it's a real Copilot CLI tool value (mapped to Grep, Glob on the CC side) rather than an invented one, since nothing in field-inventory.md enumerates valid tools values to cross-check against.

Everything else checked out

  • .vale.ini scope syntax (scope: text.frontmatter.description) is a real Vale feature, confirmed against current docs — not a made-up config key.
  • PaddingPhrase (scope: text, not frontmatter-scoped) fires correctly regardless of line-wrapping, confirmed.
  • Plugin/marketplace manifest wiring for lint is structurally consistent with other plugins (version 1.1.0 matches the repo's convention for newly-added plugins; CC vs Copilot manifest split matches the established pattern).
  • No dogfooding false-positives: running vale --config .vale.ini against the two edited skill-audit/agent-audit SKILL.md files themselves returns clean.
## Review (read-only — no edits made) Verified locally by running `vale` directly (v3.15.2, installed at `/root/.local/bin/vale`) against the actual repo tree on this branch, rather than just reading the diff. Full test suite (`bash tests/run-tests.sh`) and `scripts/check-manifests.sh` both pass as claimed. ### 🔴 Blocking: multi-line folded descriptions silently defeat `text.frontmatter.description` scope `Kyberforge.DescriptionOpener` and `Kyberforge.VagueWording` are scoped to `text.frontmatter.description`. Confirmed working when `description:` is a single physical line. But the moment the folded scalar (`description: >`) spans **two or more physical lines** — which is the style used by the large majority of skills in this repo, including `skill-audit`/`agent-audit` themselves — the scope silently stops matching. Reproduced directly: ```yaml --- name: zzzskill description: > Use when the user wants to review a skill they wrote, says "audit this skill". This skill helps with reviewing things and utilize best practices. --- ``` ``` $ vale --config .vale.ini SKILL.md ✔ 0 errors, 0 warnings and 0 suggestions in 1 file. ``` That's an obvious `DescriptionOpener` + two `VagueWording` hit, and Vale reports clean. A single-line version of the identical text triggers correctly. This means a full sweep of the repo's own 49 skill/agent files currently returns 0/0/0 — not because the repo is clean, but because the check that's supposed to cover most of the Description dimension never fires for anything using the folded style. Since `skill-audit`/`agent-audit` treat a clean Vale run as "no additional findings" for that dimension, this is a silent false negative, not a no-op — worse than not having the check at all, since it now reads as evidence of cleanliness. Worth a real regression test asserting Vale fires against a fixture with a known-bad multi-line description — nothing in the added test coverage would have caught this, which is presumably how it shipped. ### 🟠 Gap: pre-commit hook glob crosses directory boundaries the audit-invocation path deliberately avoids CONTEXT.md/the SKILL.md edits explicitly acknowledge that `plugins/*/agents/*.md`-style globs match nested paths like `docs/research/examples/**/agents/*.md`, and mitigate it by always scoping `skill-audit`/`agent-audit`'s Step 1 `vale` invocation to a specific target file rather than a sweep. Confirmed that mitigation holds for the audit-skill path. It does **not** hold for the new `.pre-commit-config.yaml` hook (`vale-audit-prefilter`), whose `files` regex (`^plugins/.*/(skills/.*/SKILL\.md|agents/.*\.md)$`) has the same cross-boundary behavior and is not manually scoped — pre-commit runs it automatically against whatever staged files match. Confirmed this matches (and would lint) e.g. `plugins/kyberforge/docs/research/examples/skill-write/skill-creator/agents/*.md` (upstream reference material, explicitly documented as "not shipped with the plugin") and `plugins/kyberforge/skills/skill-author/assets/templates/SKILL.md` (a placeholder template). Also confirmed Vale exits 1 on any `error`-level alert, which `language: unsupported` pre-commit hooks treat as hook failure. Net effect: a future commit touching either of those paths — upstream example content or the FILL-IN template — that happens to contain house-style-violating phrasing (plausible for both, since neither is written to Kyberforge's own description conventions) will get blocked by a hook whose stated purpose is auditing Kyberforge-authored skills/agents, not arbitrary reference docs. Suggest tightening the `files` regex (e.g. excluding `docs/research/` and `assets/`) to match the audit path's scoping intent. ### 🟡 Minor / worth confirming before merge `plugins/lint/agents/lint-runner.agent.md` uses `tools: ["execute", "read", "search"]`. The two existing orchestrator agent pairs (`git-orchestrate`, `gitea-orchestrate`) only ever use `execute`/`read`/`edit` — `search` is a new tool identifier with no precedent elsewhere in the repo. Worth double-checking it's a real Copilot CLI tool value (mapped to `Grep, Glob` on the CC side) rather than an invented one, since nothing in `field-inventory.md` enumerates valid `tools` values to cross-check against. ### Everything else checked out - `.vale.ini` scope syntax (`scope: text.frontmatter.description`) is a real Vale feature, confirmed against current docs — not a made-up config key. - `PaddingPhrase` (scope: `text`, not frontmatter-scoped) fires correctly regardless of line-wrapping, confirmed. - Plugin/marketplace manifest wiring for `lint` is structurally consistent with other plugins (version `1.1.0` matches the repo's convention for newly-added plugins; CC vs Copilot manifest split matches the established pattern). - No dogfooding false-positives: running `vale --config .vale.ini` against the two edited `skill-audit`/`agent-audit` SKILL.md files themselves returns clean.
Defame1297 added 1 commit 2026-07-24 10:17:20 +00:00
Vale's text.frontmatter.description scope silently stops matching once
the description is a YAML block scalar spanning 2+ physical lines —
the style used by most skills/agents in this repo. scripts/vale-wrap.sh
flattens the description to one line in a scratch copy (preserving the
repo-relative path and total line count) before invoking real vale, and
both audit skills plus the pre-commit hook now call it instead of vale
directly. Also tightens the pre-commit hook's file glob to single path
segments so it can't cross into docs/research examples or asset
templates the way the audit skills' scoped invocations already avoid.

Addresses PR #85 review feedback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxG5T8EJDgkABXxuneuFfn
Author
Collaborator

Pushed 0bbb965 addressing the correctness/scoping items from both reviews. Deferred the larger architecture questions (moving styles/ into the plugin, genericizing the plugin/agent naming, expanding coverage via research) to a follow-up — flagging here rather than rolling them into this PR silently.

🔴 Blocking bug (my review) — fixed. Confirmed and reproduced: text.frontmatter.description silently stops matching once the description spans 2+ physical lines (a YAML block scalar), which is how ~30 of the ~44 skill/agent files in this repo write it — meaning the repo-wide sweep was reading as clean while the check simply wasn't running. Added scripts/vale-wrap.sh, which flattens the description to one physical line in a scratch copy (padding with blank lines so no other line number shifts) before handing off to real vale. Both audit skills' Step 1 and the pre-commit hook now call it instead of vale directly. tests/test-vale-wrap.sh regression-tests it — verified it fails without the fix and passes with it. Also added the gotcha to vale-config's SKILL.md so it's documented for any other project using this style, not just here.

🟠 Pre-commit glob gap (my review) — fixed. Tightened ^plugins/.*/... to ^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$ — single path segments instead of .*, so it can't cross into docs/research/examples/**/agents/*.md or assets/templates/SKILL.md the way the old glob did. Verified against the tracked tree: matches exactly the 45 real skill/agent files, excludes both problem paths.

🟡 lint-runner's search tool (my review) — no change, confirmed correct. search is agent-author's own documented Copilot alias for Grep/Glob (see assets/templates/copilot.agent.md). git-orchestrate/gitea-orchestrate just never needed Grep/Glob in CC, so they never had precedent for it — not a sign this was wrong.

Manual review item 1 (why split the Copilot check) — by design, not a gap: KyberforgeCopilot.ProactivePhrase flags Use proactively, which is 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. Reworded agent-audit's Step 1 prose to state this directly instead of leaving it implicit.

Manual review item 2 (trim the Vale-styles prose) — done, in both audit skills' Step 1.

Manual review item 6 (cross-check vale-config/vale-run against actual wiring) — done. vale-run didn't need changes. vale-config was missing the frontmatter multi-line gotcha entirely (see above) — added.

Deferred, not in this push:

  • Item 3 (relocate styles//.vale.ini into plugins/lint/ for portability to other repos) — real restructuring, needs its own pass.
  • Item 4 (genericize plugin.json/lint-runner naming away from Vale-specific) — same.
  • Item 7 (research-driven coverage expansion) — open-ended, separate effort.
  • Item 5 was a note to self, not actionable by me.

Full test suite (bash tests/run-tests.sh) and scripts/check-manifests.sh still pass; ran the actual pre-commit run vale-audit-prefilter against real files to confirm the hook config change works end-to-end.

Pushed `0bbb965` addressing the correctness/scoping items from both reviews. Deferred the larger architecture questions (moving `styles/` into the plugin, genericizing the plugin/agent naming, expanding coverage via research) to a follow-up — flagging here rather than rolling them into this PR silently. **🔴 Blocking bug (my review) — fixed.** Confirmed and reproduced: `text.frontmatter.description` silently stops matching once the description spans 2+ physical lines (a YAML block scalar), which is how ~30 of the ~44 skill/agent files in this repo write it — meaning the repo-wide sweep was reading as clean while the check simply wasn't running. Added `scripts/vale-wrap.sh`, which flattens the description to one physical line in a scratch copy (padding with blank lines so no other line number shifts) before handing off to real `vale`. Both audit skills' Step 1 and the pre-commit hook now call it instead of `vale` directly. `tests/test-vale-wrap.sh` regression-tests it — verified it fails without the fix and passes with it. Also added the gotcha to `vale-config`'s SKILL.md so it's documented for any other project using this style, not just here. **🟠 Pre-commit glob gap (my review) — fixed.** Tightened `^plugins/.*/...` to `^plugins/[^/]+/(skills/[^/]+/SKILL\.md|agents/[^/]+\.md)$` — single path segments instead of `.*`, so it can't cross into `docs/research/examples/**/agents/*.md` or `assets/templates/SKILL.md` the way the old glob did. Verified against the tracked tree: matches exactly the 45 real skill/agent files, excludes both problem paths. **🟡 `lint-runner`'s `search` tool (my review) — no change, confirmed correct.** `search` is `agent-author`'s own documented Copilot alias for `Grep`/`Glob` (see `assets/templates/copilot.agent.md`). `git-orchestrate`/`gitea-orchestrate` just never needed Grep/Glob in CC, so they never had precedent for it — not a sign this was wrong. **Manual review item 1 (why split the Copilot check)** — by design, not a gap: `KyberforgeCopilot.ProactivePhrase` flags `Use proactively`, which is 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. Reworded `agent-audit`'s Step 1 prose to state this directly instead of leaving it implicit. **Manual review item 2 (trim the Vale-styles prose)** — done, in both audit skills' Step 1. **Manual review item 6 (cross-check vale-config/vale-run against actual wiring)** — done. `vale-run` didn't need changes. `vale-config` was missing the frontmatter multi-line gotcha entirely (see above) — added. **Deferred, not in this push:** - Item 3 (relocate `styles/`/`.vale.ini` into `plugins/lint/` for portability to other repos) — real restructuring, needs its own pass. - Item 4 (genericize `plugin.json`/`lint-runner` naming away from Vale-specific) — same. - Item 7 (research-driven coverage expansion) — open-ended, separate effort. - Item 5 was a note to self, not actionable by me. Full test suite (`bash tests/run-tests.sh`) and `scripts/check-manifests.sh` still pass; ran the actual `pre-commit run vale-audit-prefilter` against real files to confirm the hook config change works end-to-end.
Defame1297 added 1 commit 2026-07-24 12:25:36 +00:00
lint-runner's description already promised other linters could be added
without changing its own contract, but Process hardcoded vale-config/
vale-run and .vale.ini by name. Switch to <linter>-config/<linter>-run
naming-convention dispatch so the promise holds. Drop the explicit
Vale callout from the plugin manifests' description/keywords to match.

Addresses a deferred item from PR #85 review.
Defame1297 added 1 commit 2026-07-24 20:06:04 +00:00
Deferred item from PR #85 review. Per ADR-0013: cherry-picks two low-noise
rules from trialing write-good/alex against the real corpus (VagueQualifier,
SentenceOpenerThereIs) into styles/Kyberforge rather than adopting either
package wholesale (both are tuned for blog prose and were noisy on this
repo's terse, imperative instruction files - see the ADR's rejected-rule
list). Adds a new skill-size-check pre-commit hook enforcing agentskills.io's
500-line/5,000-token SKILL.md ceiling, currently unenforced. Fixes the 28
resulting violations across 20 existing SKILL.md/agent files so the
enforcing pre-commit hook lands clean.

governance.md/CONTROLS.md were evaluated and excluded as rule sources -
they're org/CI-infrastructure controls, not prose patterns Vale can express.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUDczvw1H3eEeMD29Q9Lbi
Author
Collaborator

Resolves the deferred "expand Vale coverage" item from this PR's review, per grill session + ADR-0013 (docs/adr/0013-vale-harness-scope-and-rule-sources.md):

  • Trialed write-good/alex against the real SKILL.md/agent-file corpus; cherry-picked 2 low-noise rules into styles/Kyberforge/ (VagueQualifier, SentenceOpenerThereIs) — 7 other candidate rules rejected as too noisy on this repo's terse, imperative-instruction style (e.g. write-good.Passive, alex.ProfanityUnlikely flagging words like "hook"/"crash").
  • Added a skill-size-check pre-commit hook enforcing agentskills.io's 500-line/5,000-token SKILL.md ceiling (previously unenforced).
  • Fixed the 28 violations the new rules surfaced across 20 existing SKILL.md/agent files so the enforcing hook lands clean.
  • Governance docs (governance.md, docs/research/governance_principles/CONTROLS.md) were evaluated and explicitly excluded as rule sources — they're org/CI-infrastructure controls, not prose patterns Vale can express.

File scope and enforcement model are unchanged (SKILL.md + agent files only, rules enforce immediately via the existing pre-commit hook, no new trial tier).

Resolves the deferred "expand Vale coverage" item from this PR's review, per grill session + ADR-0013 (`docs/adr/0013-vale-harness-scope-and-rule-sources.md`): - Trialed `write-good`/`alex` against the real SKILL.md/agent-file corpus; cherry-picked 2 low-noise rules into `styles/Kyberforge/` (`VagueQualifier`, `SentenceOpenerThereIs`) — 7 other candidate rules rejected as too noisy on this repo's terse, imperative-instruction style (e.g. `write-good.Passive`, `alex.ProfanityUnlikely` flagging words like "hook"/"crash"). - Added a `skill-size-check` pre-commit hook enforcing agentskills.io's 500-line/5,000-token `SKILL.md` ceiling (previously unenforced). - Fixed the 28 violations the new rules surfaced across 20 existing SKILL.md/agent files so the enforcing hook lands clean. - Governance docs (`governance.md`, `docs/research/governance_principles/CONTROLS.md`) were evaluated and explicitly excluded as rule sources — they're org/CI-infrastructure controls, not prose patterns Vale can express. File scope and enforcement model are unchanged (SKILL.md + agent files only, rules enforce immediately via the existing pre-commit hook, no new trial tier).
Claude reviewed 2026-07-24 20:39:26 +00:00
Claude left a comment
Author
Collaborator

Review round 2 (read-only — no edits made)

Re-reviewed 0bbb965, 28058d3, 2eb13f7 against the round-1 findings. Verified by execution against the real tree with vale 3.15.2, not by reading the diff: full suite is green (9 passed, 0 failed; bats 125/125), check-manifests.sh exit 0, claude plugin validate --strict clean on all six plugins, branch 11 ahead / 0 behind main (fast-forward).

The test suite passing is not evidence the harness works. Three of the four items below are things the suite asserts and the tree does not do.


🔴 Blocking 1 — the round-1 blocking bug is still live for 58% of the corpus

scripts/vale-wrap.sh:68 fixes the folded-scalar bug and then reintroduces it through the escaping.

json.dumps emits \" for embedded quotes, \\ for backslashes, \uXXXX for non-ASCII. Vale's text.frontmatter.description scope stops matching entirely once the double-quoted value contains any backslash escape. The file still parses as valid YAML — so the inline comment at :65-67 ("always a well-formed YAML value regardless of colons, quotes, or backslashes") is correct about validity and wrong about the outcome. Vale lints nothing and exits 0.

Reproduced on a fixture whose description carries the same "audit this skill" quoting style the real audit skills use:

description: >
  Use when the user wants to review a skill, says "audit this skill".
  This skill helps with reviewing and can utilize best practices.
bare vale : 0 errors, 0 warnings          exit=0
vale-wrap : 0 errors, 0 warnings          exit=0   <- the fix reports nothing

Delete the two " characters and nothing else — same wrapper, same config:

vale-wrap : 3:92  warning  Kyberforge.VagueWording  'helps with'
            3:121 warning  Kyberforge.VagueWording  'utilize'

Blast radius, computed over the 45 tracked in-scope files:

in-scope files                        : 45
folded multi-line descriptions        : 28
silently unlinted by the wrapper      : 26   (58% of corpus, 26 of 28 folded)

That set includes skill-audit, agent-audit, skill-author, agent-author, forge, marketplace-author, plugin-author, all 7 gitea skills, all 6 git skills, all 3 core skills, and both new lint skills. Round 1's finding was "a clean Vale run reads as evidence of cleanliness when the check never fired." That is still true, for most of the repo, with the fix in place.

The reason tests/test-vale-wrap.sh passes: its fixture puts a colon in the description but no double quote. A colon alone survives json.dumps unescaped, so the test exercises the one punctuation class that happens to work.

Fix direction: emit a single-quoted YAML scalar ('…' with '' doubling) rather than json.dumps, and add a fixture with ", \, and a non-ASCII character.

🔴 Blocking 2 — a linter false positive silently changed a shipped skill's behavior

plugins/bin/skills/caveman/SKILL.md:18 is caveman mode's list of words it instructs the agent to delete. 2eb13f7 rewrote it:

-filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to)
+filler (just/really/basically/actually/literally), pleasantries (sure/certainly/no worries/happy to)

simply and of course were flagged as uses when they were mentions inside a drop-list, and the fix removed them from the list. Caveman mode no longer strips "simply" or "of course". Nothing about that line was vague prose — the rule had nothing legitimate to catch, and the edit is a functional regression to a shipped skill made solely to silence it.

This is the failure mode to design against, not a one-off: a prose linter over instruction files will keep hitting use/mention conflation, because these files quote the words they govern. Revert this line and guard it (TokenIgnores, or <!-- vale off -->).

Same root cause, meaning-changing, lower severity:

  • write-docs/SKILL.md:64 — non-obvious invariants → hidden invariants. Different concept: "non-obvious" = present but easy to miss; "hidden" = not visible at all. The instruction now asks the writer to extract what by definition can't be read out of the code. non-obvious is house vocabulary here — it's in core/instructions/coding.md:4, ADR-FORMAT.md, commit-template.md, and content-guide.md. The rule flags the repo's own term of art, because - is a word boundary.
  • tdd/SKILL.md:94 — move complexity behind simple interfaces → narrow interfaces. That's Ousterhout's canonical phrasing, and "Deepen modules" on the same line is the deliberate callback. "Narrow" is a different property (few methods). The phrase no longer matches any sibling doc.
  • gitea-workflow/SKILL.md:59 — "requests that name a capability but not obviously which skill owns it" → "without a clear owning skill". Inverts the precondition: the table directly below assigns an owner to every capability, so a router reading the new wording concludes the index never applies.
  • marketplace-author/SKILL.md:185 — State clearly before proceeding: → State the following before proceeding:, on a destructive marketplace-removal confirmation gate. Lost the directive about how to state it.
  • forge/SKILL.md:35 — "if the intent genuinely spans several" → "spans multiple". several is a standalone pronoun; multiple is not. Dangling determiner.

Five more are pure synonym dodges where the rule caught nothing real (usually→typically, tiny→minimal, etc.), and two leave a linted SKILL.md contradicting its own unlinted references/ file it delegates to (prototype/UI.md:3 still says "several"; gitea-issues/references/enrichments.md:50,53 still says "clearly fits" while SKILL.md:75 was weakened to "well-matched").

🔴 Blocking 3 — the Step 1 command block cannot execute from any working directory

skill-audit/SKILL.md:34-38 and agent-audit/SKILL.md:36-40:

bash scripts/validate.sh <skill-dir>
bash scripts/validate-provenance.sh <skill-dir>
scripts/vale-wrap.sh --config .vale.ini <skill-dir>/SKILL.md

Lines 1–2 resolve scripts/ to the skill's own bundled directory. Line 3 resolves scripts/ to the repo root. vale-wrap.sh is not in either audit skill's scripts/ (confirmed: only validate.sh and validate-provenance.sh are). There is no cwd where all three run:

cwd = repo root : bash scripts/validate.sh …      -> No such file or directory (127)
                  scripts/vale-wrap.sh …          -> ok
cwd = skill dir : bash scripts/validate.sh …      -> All checks passed.
                  scripts/vale-wrap.sh …          -> No such file or directory (127)

Introduced by 0bbb965 — the pre-fix line was vale --config .vale.ini …, which needed no path. The prose at skill-audit/SKILL.md:44 ("vale-wrap.sh runs from the repo root") contradicts lines 35–36 in the same section.

🔴 Blocking 4 — VagueQualifier has a ~100% false-positive rate on this repo's own voice

Ran the two new rules over the 280 tracked .md files outside the linted globs — same authors, same register:

196 alerts:  168 VagueQualifier   24 SentenceOpenerThereIs   4 PaddingPhrase
top tokens:  obvious 29, substantially 18, clearly 18, simple 17, easy 14,
             usually 10, very 8, significantly 8, completely 8

Every context for the top eight tokens was read. Essentially none identify vague writing:

  • obvious — 24 unique contexts, all correct: non-obvious (7×), isn't obvious, stating the obvious, deviations from the obvious path.
  • substantially — includes "substantially similar", the copyright legal term of art (ai-governance-research.md:435,455), plus quantitative research findings. Flagging a legal term of art as filler is a category error.
  • clearly — core/AGENTS.md:8 "When disagreeing, say so clearly"; enrichments.md:50 "If a milestone clearly fits". Both are precise confidence thresholds.
  • usually — "usually main or develop", "500 | usually transient". Correct frequency claims.

ADR-0013 claims these rules are "proven low-noise against the existing corpus." They are low-noise only against the 16-file linted subset, and there is no trial artifact in the tree to reproduce the claim — .vale.ini has no Packages, and write-good/alex were never synced, so which rules were rejected and why is unrecorded. The commit message cites "the ADR's rejected-rule list"; no such list exists in the ADR.

Suggest cutting the tokens that measure as pure noise here — obvious, obviously, clearly, usually, mostly, several, various, substantially, significantly, relatively, largely, fairly, simple, easy — and keeping the defensible filler: of course, everyone knows, interestingly, surprisingly, remarkably, exceedingly, very, quite, huge, vast, excellent.


🟠 Gaps

Only 1 of 6 rules can actually block a commit. Vale exits non-zero on error alerts only. DescriptionOpener is error; the other five are warning. MinAlertLevel affects display, not exit code — and pre-commit discards hook stdout on success, so warnings are invisible:

direct        : 3:35 VagueWording 'utilize' / 3:60 'helps with'   exit=0
via pre-commit: Vale audit prefilter......................Passed  (no output)

ADR-0013 and CONTEXT.md:77 describe the hook as "blocking immediately." Empirically it blocks on DescriptionOpener only. Set --minAlertLevel=warning on the hook entry, or raise the rule levels.

A blank line inside a folded description corrupts the scratch copy and hard-fails the commit. The body regex at vale-wrap.sh:61 can't cross a blank line, so a two-paragraph description matches only the first; the second is left orphaned after the scalar. Output is invalid YAML → yaml: line 5: did not find expected key, wrapper exit 2, hook fails against a valid file. 0/45 files have this shape today, but "Use when… / Do not use when…" split across paragraphs is natural for exactly these descriptions.

--config=X (equals form) breaks. Only the two-argv form is rewritten to an absolute path; the equals form passes through and dies against the temp cwd (E100 path '.vale.ini' does not exist). Bare vale accepts it.

Zero file arguments hangs forever. exec vale "${vale_args[@]}" with no positional falls through to stdin and blocks indefinitely (verified: timeout 8 → exit 124). Reachable whenever a caller's file list filters to empty. Needs a guard plus </dev/null.

Absolute paths silently no-op. vale-wrap.sh:32-36 requires -f "$repo_root/$arg", so an absolute path never enters files[], is never flattened, and can't match .vale.ini's anchored globs → exit 0, no output. Same for a typo'd path and for invocation from a subdirectory. This matters because the Step 1 instruction is scripts/vale-wrap.sh … <skill-dir>/SKILL.md and an agent substituting an absolute skill dir — routine — gets a clean pass and reports "no Vale findings."

Item 3 (styles in the plugin) is still open, and it leaves the prefilter dead for every external consumer. styles/ and .vale.ini exist only at repo root. skill-audit/SKILL.md:37 and agent-audit/SKILL.md:39 hardcode scripts/vale-wrap.sh --config .vale.ini. Verified in a scratch external repo: No such file or directory (127), and with the wrapper present but no config, E100 path '…/.vale.ini' does not exist. So for anyone installing kyberforge@holocron elsewhere, Step 1 degrades to "skip and fall back to judgment" and can never fire. Fine to defer — but it's recorded nowhere, and ADR-0013 should carry it as a known limitation. Precedent for the fix exists in skill-author/assets/templates/ + ${CLAUDE_PLUGIN_ROOT}.

Item 4 is incomplete — both marketplace manifests were missed. .claude-plugin/marketplace.json:43 and .github/plugin/marketplace.json:43 still read "…running linters, starting with Vale." while both plugin.json files were genericized. lint is now the only plugin in the repo whose marketplace description diverges from its plugin.json — the other five match byte-for-byte. check-manifests.sh doesn't catch this (it validates path resolution only).

vale-run and lint-runner bypass the wrapper. vale-run/SKILL.md:30-31 documents vale <path-or-glob>; lint-runner.md:21 routes to it. Neither mentions the wrapper. A lint-runner sweep therefore reports clean on files the commit hook would reject. CONTEXT.md:75 enumerates the wrapper's call sites as "both skills' Step 1, and the pre-commit hook" — these two are absent, which reads as an oversight rather than a decision. Related: vale-run/references/troubleshooting.md:57-70 recommends the upstream errata-ai/vale pre-commit hook, which calls vale directly — correct generic advice, but following it here reintroduces the bug the wrapper exists to fix.

The vale binary is an undocumented hard prerequisite. With vale off PATH: scripts/vale-wrap.sh: line 79: vale: command not found, exit 127, surfaced by pre-commit as a bare hook failure with no install pointer. AGENTS.md Setup mentions only pre-commit install. A fresh clone hard-blocks every commit touching a SKILL.md or agent file until the developer works out that Vale is needed. Should be named in AGENTS.md Setup.

skill-size-check is ~70% more permissive than the ceiling it advertises. scripts/skill-size-check.sh:10-13 argues the word proxy is "conservative." The inequality is backwards — words are always fewer than tokens. Measured on this repo's 39-file SKILL.md corpus: 6.80 chars/word → ~1.70 tokens/word, so MAX_WORDS=5000 ≈ 8,500 tokens against a stated 5,000. skill-author/SKILL.md is at 81% of the real token budget but only 50% of the word budget — the hook cannot warn before it blows through. The error string at :30 prints a word count and calls it a token ceiling. Either set MAX_WORDS ≈ 2,900–3,800, or drop the token claim. (Source citation itself checks out: skill-authoring.md:163 says "under 500 lines and 5,000 tokens" verbatim.)

The two new rules are not wired into the audit skills' mapping prose. skill-audit/SKILL.md:56 and agent-audit/SKILL.md:55-57,64 name only DescriptionOpener/VagueWording/PaddingPhrase/ProactivePhrase. An audit run has no instruction for what to do with a VagueQualifier or SentenceOpenerThereIs alert.

lint-runner has no provenance chain. Per ADR-0010, plugin-scope agents carry source_keys against a plugin-root sources.md. git-orchestrate (6 keys) and gitea-orchestrate (4 keys) both comply; plugins/lint/sources.md doesn't exist and the agent pair has no source_keys. Invisible to tooling — validate-provenance.sh exits 0 silently when no provenance data exists. The lint skills are wired correctly, so the agent is the only artifact in the plugin without it.

Docs contradict the commit that added them.

  • CONTEXT.md:77 calls the write-good/alex rules "(still pending implementation)" — same commit implements them. CONTEXT.md:73 still says Vale "only" covers the four #84 checks and that "body discipline stays LLM judgment," which the two new body-wide rules now falsify. Neither new rule is named anywhere in CONTEXT.md.
  • ADR-0013's Consequences (L79–86) is written forward-looking about its own commit: "will gain new rule files once the (separate, later) implementation pass…", "a new pre-commit hook (name TBD by the implementer)" (it's skill-size-check), and a "follow-up work — not part of this ADR" list whose every item is in 2eb13f7.
  • plugins/kyberforge/docs/README.md:20 still indexes research/docs/vale/, which moved to plugins/lint/ in e1a5403. The directory doesn't exist; plugins/lint/docs/ has no replacement index.

🟡 Minor

  • SentenceOpenerThereIs.yml:7 — first regex alternative is dead. (?:[;-]\s)There\s(is|are) is fully subsumed by the \bThere\s(is|are)\b alternative: identical alert count, identical flagged sites. Its only effect is widening the captured span, which corrupts %s into Don't start a sentence with '; There is'. Also [;-] is ASCII hyphen — this repo's prose uses em dashes, so even the intended target is the wrong character.
  • That rule doesn't implement "start a sentence." No ^, no scope: sentence; ignorecase: false is the only proxy for sentence-initial position. Over-matches headings, mid-sentence, table cells, link text; under-matches there is lowercase, There's, There exist.
  • VagueQualifier and SentenceOpenerThereIs lint all YAML frontmatter keys. They're the only two rules using bare scope: text; the other four are text.frontmatter.description. Verified a skill named very-simple-skill gets flagged on its name: line — unfixable without renaming the directory. No current name collides, but it also means the full 30-token list is now live against every skill's description:.
  • The documented root cause is wrong about |. CONTEXT.md:75 and vale-config/SKILL.md:25 both say the scope breaks for "a block scalar (>/|)". Only > (folded) breaks — bare vale matches every line of a | literal block. vale-config is shipped content agents treat as ground truth.
  • vale-config/SKILL.md:22 asserts a fresh config "will fail or find nothing until vale sync runs" — this repo declares no Packages and works fine; vale sync is a no-op here. Scope the claim to package-based styles.
  • skill-size-check.sh boundary is inclusive (>), so exactly 500 lines passes where the source says "under 500"; wc -l undercounts a file with no trailing newline, disagreeing with skill-audit/scripts/validate.sh:137 (splitlines()) at the boundary; nonexistent paths, directories, and zero args all exit 0 silently.
  • lint-runner's genericization is a naming convention, not dispatch — no registry, no discovery, sample size of one. The <linter>-config/<linter>-run guard at :24 is the substantive part and is good. But frontmatter :4 still names vale-config/vale-run concretely, contradicting the <linter>-* body two lines down.
  • lint-runner is the repo's only agent pair with a non-1:1 tool mapping (Grep+Glob → single search). Plausibly correct, but it's the sole precedent and unverified against field-inventory.md.

Test coverage

Both new suites assert the shape of the implementation, not the spec.

test-vale-wrap.sh — fixture deliberately includes a colon but no double quote, which is exactly why it passes while 26 real files silently fail. Uncovered: quotes/backslashes/unicode, blank line in a folded block, --config= form, zero args, absolute/nonexistent/subdirectory invocation, | and >-/|+ variants, plain and quoted multi-line descriptions, exit-code fidelity. Test 3's line-preservation assertion (MAX_LINE <= ORIG_LINES) passes trivially if flattening drops all alerts.

test-skill-size-check.sh — fixtures are 10 lines / 604 lines / ~6,004 words, all 20%+ from the threshold. Flipping > to >=, or changing MAX_LINES to 550, passes all three tests. No boundary pairs (500/501, 5000/5001), no zero-arg/missing-path/empty-file cases.

Credit where due: test-vale-wrap.sh is a real regression test for the bug it was written for — neutering the flattening in a copy flips test 2 to FAIL. The gap is coverage, not construction.


What genuinely holds

  • Hook regex (round-1 finding 2) is exact. Matches 45 files, excludes all 7 docs/research/examples/** files and assets/templates/SKILL.md, misses zero legitimate ones.
  • Folded-scalar flattening works for the unescaped case, >-, description: not first key, nested metadata: description: correctly left alone, --- rules in body, CRLF, and line-number preservation.
  • KyberforgeCopilot scoping is correct and the round-1 rationale (item 1) is right: Use proactively is live in CC, dead in Copilot. Verified it fires on .agent.md and not .md.
  • Code blocks, inline code, link URLs, and HTML comments are correctly excluded from scope: text.
  • Token lists are properly word-boundary anchored — simple does not flag simplify, various does not flag variation. The separate simple/simply entries are each necessary.
  • The "28 violations" figure is accurate (parent commit: 28 alerts; HEAD: 0). ADR-0013 conforms to the repo's ADR format. File scope is unchanged as claimed. Rule schemas are valid; %s interpolation correct for both tokens and raw.
  • Manifest pair for lint agrees on name/version/paths; agent pair diff is a single line (the tools mapping); agent-audit's own validators pass on it.

Recommendation: request changes. Blocking 1 is the round-1 finding unfixed — same silent-clean signature, 58% of the corpus. Blocking 2 is a shipped skill that behaves differently now because a linter was wrong. Blocking 4 says the rule generating those edits is wrong ~100% of the time on this repo's voice, which makes the 28-violation cleanup a net negative on prose quality. I'd land 1 and 3 as fixes, revert the 2/4 prose edits wholesale, cut the token list down before re-running, and split the size-check into its own PR.

## Review round 2 (read-only — no edits made) Re-reviewed `0bbb965`, `28058d3`, `2eb13f7` against the round-1 findings. Verified by execution against the real tree with vale 3.15.2, not by reading the diff: full suite is green (`9 passed, 0 failed`; bats 125/125), `check-manifests.sh` exit 0, `claude plugin validate --strict` clean on all six plugins, branch 11 ahead / 0 behind `main` (fast-forward). The test suite passing is not evidence the harness works. Three of the four items below are things the suite asserts and the tree does not do. --- ## 🔴 Blocking 1 — the round-1 blocking bug is still live for 58% of the corpus `scripts/vale-wrap.sh:68` fixes the folded-scalar bug and then reintroduces it through the escaping. `json.dumps` emits `\"` for embedded quotes, `\\` for backslashes, `\uXXXX` for non-ASCII. **Vale's `text.frontmatter.description` scope stops matching entirely once the double-quoted value contains any backslash escape.** The file still parses as valid YAML — so the inline comment at `:65-67` ("always a well-formed YAML value regardless of colons, quotes, or backslashes") is correct about validity and wrong about the outcome. Vale lints nothing and exits 0. Reproduced on a fixture whose description carries the same `"audit this skill"` quoting style the real audit skills use: ```yaml description: > Use when the user wants to review a skill, says "audit this skill". This skill helps with reviewing and can utilize best practices. ``` ``` bare vale : 0 errors, 0 warnings exit=0 vale-wrap : 0 errors, 0 warnings exit=0 <- the fix reports nothing ``` Delete the two `"` characters and nothing else — same wrapper, same config: ``` vale-wrap : 3:92 warning Kyberforge.VagueWording 'helps with' 3:121 warning Kyberforge.VagueWording 'utilize' ``` Blast radius, computed over the 45 tracked in-scope files: ``` in-scope files : 45 folded multi-line descriptions : 28 silently unlinted by the wrapper : 26 (58% of corpus, 26 of 28 folded) ``` That set includes `skill-audit`, `agent-audit`, `skill-author`, `agent-author`, `forge`, `marketplace-author`, `plugin-author`, all 7 gitea skills, all 6 git skills, all 3 core skills, and both new `lint` skills. Round 1's finding was "a clean Vale run reads as evidence of cleanliness when the check never fired." That is still true, for most of the repo, with the fix in place. The reason `tests/test-vale-wrap.sh` passes: its fixture puts a colon in the description but no double quote. A colon alone survives `json.dumps` unescaped, so the test exercises the one punctuation class that happens to work. Fix direction: emit a single-quoted YAML scalar (`'…'` with `''` doubling) rather than `json.dumps`, and add a fixture with `"`, `\`, and a non-ASCII character. ## 🔴 Blocking 2 — a linter false positive silently changed a shipped skill's behavior `plugins/bin/skills/caveman/SKILL.md:18` is caveman mode's list of words it instructs the agent to **delete**. `2eb13f7` rewrote it: ```diff -filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to) +filler (just/really/basically/actually/literally), pleasantries (sure/certainly/no worries/happy to) ``` `simply` and `of course` were flagged as *uses* when they were *mentions* inside a drop-list, and the fix removed them from the list. Caveman mode no longer strips "simply" or "of course". Nothing about that line was vague prose — the rule had nothing legitimate to catch, and the edit is a functional regression to a shipped skill made solely to silence it. This is the failure mode to design against, not a one-off: a prose linter over instruction files will keep hitting use/mention conflation, because these files quote the words they govern. Revert this line and guard it (`TokenIgnores`, or `<!-- vale off -->`). Same root cause, meaning-changing, lower severity: - `write-docs/SKILL.md:64` — `non-obvious invariants` → `hidden invariants`. Different concept: "non-obvious" = present but easy to miss; "hidden" = not visible at all. The instruction now asks the writer to extract what by definition can't be read out of the code. `non-obvious` is house vocabulary here — it's in `core/instructions/coding.md:4`, `ADR-FORMAT.md`, `commit-template.md`, and `content-guide.md`. The rule flags the repo's own term of art, because `-` is a word boundary. - `tdd/SKILL.md:94` — `move complexity behind simple interfaces` → `narrow interfaces`. That's Ousterhout's canonical phrasing, and "Deepen modules" on the same line is the deliberate callback. "Narrow" is a different property (few methods). The phrase no longer matches any sibling doc. - `gitea-workflow/SKILL.md:59` — "requests that name a capability but not obviously which skill owns it" → "without a clear owning skill". Inverts the precondition: the table directly below assigns an owner to every capability, so a router reading the new wording concludes the index never applies. - `marketplace-author/SKILL.md:185` — `State clearly before proceeding:` → `State the following before proceeding:`, on a destructive marketplace-removal confirmation gate. Lost the directive about *how* to state it. - `forge/SKILL.md:35` — "if the intent genuinely spans several" → "spans multiple". `several` is a standalone pronoun; `multiple` is not. Dangling determiner. Five more are pure synonym dodges where the rule caught nothing real (`usually`→`typically`, `tiny`→`minimal`, etc.), and two leave a linted `SKILL.md` contradicting its own unlinted `references/` file it delegates to (`prototype/UI.md:3` still says "several"; `gitea-issues/references/enrichments.md:50,53` still says "clearly fits" while SKILL.md:75 was weakened to "well-matched"). ## 🔴 Blocking 3 — the Step 1 command block cannot execute from any working directory `skill-audit/SKILL.md:34-38` and `agent-audit/SKILL.md:36-40`: ```bash bash scripts/validate.sh <skill-dir> bash scripts/validate-provenance.sh <skill-dir> scripts/vale-wrap.sh --config .vale.ini <skill-dir>/SKILL.md ``` Lines 1–2 resolve `scripts/` to the skill's **own** bundled directory. Line 3 resolves `scripts/` to the **repo root**. `vale-wrap.sh` is not in either audit skill's `scripts/` (confirmed: only `validate.sh` and `validate-provenance.sh` are). There is no cwd where all three run: ``` cwd = repo root : bash scripts/validate.sh … -> No such file or directory (127) scripts/vale-wrap.sh … -> ok cwd = skill dir : bash scripts/validate.sh … -> All checks passed. scripts/vale-wrap.sh … -> No such file or directory (127) ``` Introduced by `0bbb965` — the pre-fix line was `vale --config .vale.ini …`, which needed no path. The prose at `skill-audit/SKILL.md:44` ("`vale-wrap.sh` runs from the repo root") contradicts lines 35–36 in the same section. ## 🔴 Blocking 4 — `VagueQualifier` has a ~100% false-positive rate on this repo's own voice Ran the two new rules over the 280 tracked `.md` files *outside* the linted globs — same authors, same register: ``` 196 alerts: 168 VagueQualifier 24 SentenceOpenerThereIs 4 PaddingPhrase top tokens: obvious 29, substantially 18, clearly 18, simple 17, easy 14, usually 10, very 8, significantly 8, completely 8 ``` Every context for the top eight tokens was read. Essentially none identify vague writing: - `obvious` — 24 unique contexts, all correct: `non-obvious` (7×), `isn't obvious`, `stating the obvious`, `deviations from the obvious path`. - `substantially` — includes **"substantially similar"**, the copyright legal term of art (`ai-governance-research.md:435,455`), plus quantitative research findings. Flagging a legal term of art as filler is a category error. - `clearly` — `core/AGENTS.md:8` *"When disagreeing, say so clearly"*; `enrichments.md:50` *"If a milestone clearly fits"*. Both are precise confidence thresholds. - `usually` — `"usually main or develop"`, `"500 | usually transient"`. Correct frequency claims. ADR-0013 claims these rules are "proven low-noise against the existing corpus." They are low-noise only against the 16-file linted subset, and there is no trial artifact in the tree to reproduce the claim — `.vale.ini` has no `Packages`, and write-good/alex were never synced, so which rules were rejected and why is unrecorded. The commit message cites "the ADR's rejected-rule list"; no such list exists in the ADR. Suggest cutting the tokens that measure as pure noise here — `obvious`, `obviously`, `clearly`, `usually`, `mostly`, `several`, `various`, `substantially`, `significantly`, `relatively`, `largely`, `fairly`, `simple`, `easy` — and keeping the defensible filler: `of course`, `everyone knows`, `interestingly`, `surprisingly`, `remarkably`, `exceedingly`, `very`, `quite`, `huge`, `vast`, `excellent`. --- ## 🟠 Gaps **Only 1 of 6 rules can actually block a commit.** Vale exits non-zero on `error` alerts only. `DescriptionOpener` is `error`; the other five are `warning`. `MinAlertLevel` affects display, not exit code — and pre-commit discards hook stdout on success, so warnings are invisible: ``` direct : 3:35 VagueWording 'utilize' / 3:60 'helps with' exit=0 via pre-commit: Vale audit prefilter......................Passed (no output) ``` ADR-0013 and `CONTEXT.md:77` describe the hook as "blocking immediately." Empirically it blocks on `DescriptionOpener` only. Set `--minAlertLevel=warning` on the hook entry, or raise the rule levels. **A blank line inside a folded description corrupts the scratch copy and hard-fails the commit.** The body regex at `vale-wrap.sh:61` can't cross a blank line, so a two-paragraph description matches only the first; the second is left orphaned after the scalar. Output is invalid YAML → `yaml: line 5: did not find expected key`, wrapper exit 2, hook fails against a valid file. 0/45 files have this shape today, but "Use when… / Do not use when…" split across paragraphs is natural for exactly these descriptions. **`--config=X` (equals form) breaks.** Only the two-argv form is rewritten to an absolute path; the equals form passes through and dies against the temp cwd (`E100 path '.vale.ini' does not exist`). Bare vale accepts it. **Zero file arguments hangs forever.** `exec vale "${vale_args[@]}"` with no positional falls through to stdin and blocks indefinitely (verified: `timeout 8` → exit 124). Reachable whenever a caller's file list filters to empty. Needs a guard plus `</dev/null`. **Absolute paths silently no-op.** `vale-wrap.sh:32-36` requires `-f "$repo_root/$arg"`, so an absolute path never enters `files[]`, is never flattened, and can't match `.vale.ini`'s anchored globs → exit 0, no output. Same for a typo'd path and for invocation from a subdirectory. This matters because the Step 1 instruction is `scripts/vale-wrap.sh … <skill-dir>/SKILL.md` and an agent substituting an absolute skill dir — routine — gets a clean pass and reports "no Vale findings." **Item 3 (styles in the plugin) is still open, and it leaves the prefilter dead for every external consumer.** `styles/` and `.vale.ini` exist only at repo root. `skill-audit/SKILL.md:37` and `agent-audit/SKILL.md:39` hardcode `scripts/vale-wrap.sh --config .vale.ini`. Verified in a scratch external repo: `No such file or directory (127)`, and with the wrapper present but no config, `E100 path '…/.vale.ini' does not exist`. So for anyone installing `kyberforge@holocron` elsewhere, Step 1 degrades to "skip and fall back to judgment" and can never fire. Fine to defer — but it's recorded nowhere, and ADR-0013 should carry it as a known limitation. Precedent for the fix exists in `skill-author/assets/templates/` + `${CLAUDE_PLUGIN_ROOT}`. **Item 4 is incomplete — both marketplace manifests were missed.** `.claude-plugin/marketplace.json:43` and `.github/plugin/marketplace.json:43` still read *"…running linters, starting with Vale."* while both `plugin.json` files were genericized. `lint` is now the only plugin in the repo whose marketplace description diverges from its `plugin.json` — the other five match byte-for-byte. `check-manifests.sh` doesn't catch this (it validates path resolution only). **`vale-run` and `lint-runner` bypass the wrapper.** `vale-run/SKILL.md:30-31` documents `vale <path-or-glob>`; `lint-runner.md:21` routes to it. Neither mentions the wrapper. A `lint-runner` sweep therefore reports clean on files the commit hook would reject. `CONTEXT.md:75` enumerates the wrapper's call sites as "both skills' Step 1, and the pre-commit hook" — these two are absent, which reads as an oversight rather than a decision. Related: `vale-run/references/troubleshooting.md:57-70` recommends the upstream `errata-ai/vale` pre-commit hook, which calls `vale` directly — correct generic advice, but following it here reintroduces the bug the wrapper exists to fix. **The `vale` binary is an undocumented hard prerequisite.** With vale off `PATH`: `scripts/vale-wrap.sh: line 79: vale: command not found`, exit 127, surfaced by pre-commit as a bare hook failure with no install pointer. `AGENTS.md` Setup mentions only `pre-commit install`. A fresh clone hard-blocks every commit touching a SKILL.md or agent file until the developer works out that Vale is needed. Should be named in AGENTS.md Setup. **`skill-size-check` is ~70% more permissive than the ceiling it advertises.** `scripts/skill-size-check.sh:10-13` argues the word proxy is "conservative." The inequality is backwards — words are always *fewer* than tokens. Measured on this repo's 39-file SKILL.md corpus: 6.80 chars/word → ~1.70 tokens/word, so `MAX_WORDS=5000` ≈ 8,500 tokens against a stated 5,000. `skill-author/SKILL.md` is at 81% of the real token budget but only 50% of the word budget — the hook cannot warn before it blows through. The error string at `:30` prints a word count and calls it a token ceiling. Either set `MAX_WORDS` ≈ 2,900–3,800, or drop the token claim. (Source citation itself checks out: `skill-authoring.md:163` says "under 500 lines and 5,000 tokens" verbatim.) **The two new rules are not wired into the audit skills' mapping prose.** `skill-audit/SKILL.md:56` and `agent-audit/SKILL.md:55-57,64` name only `DescriptionOpener`/`VagueWording`/`PaddingPhrase`/`ProactivePhrase`. An audit run has no instruction for what to do with a `VagueQualifier` or `SentenceOpenerThereIs` alert. **`lint-runner` has no provenance chain.** Per ADR-0010, plugin-scope agents carry `source_keys` against a plugin-root `sources.md`. `git-orchestrate` (6 keys) and `gitea-orchestrate` (4 keys) both comply; `plugins/lint/sources.md` doesn't exist and the agent pair has no `source_keys`. Invisible to tooling — `validate-provenance.sh` exits 0 silently when no provenance data exists. The `lint` *skills* are wired correctly, so the agent is the only artifact in the plugin without it. **Docs contradict the commit that added them.** - `CONTEXT.md:77` calls the write-good/alex rules *"(still pending implementation)"* — same commit implements them. `CONTEXT.md:73` still says Vale "only" covers the four #84 checks and that "body discipline stays LLM judgment," which the two new body-wide rules now falsify. Neither new rule is named anywhere in CONTEXT.md. - ADR-0013's Consequences (L79–86) is written forward-looking about its own commit: *"will gain new rule files once the (separate, later) implementation pass…"*, *"a new pre-commit hook (name TBD by the implementer)"* (it's `skill-size-check`), and a "follow-up work — not part of this ADR" list whose every item is in `2eb13f7`. - `plugins/kyberforge/docs/README.md:20` still indexes `research/docs/vale/`, which moved to `plugins/lint/` in `e1a5403`. The directory doesn't exist; `plugins/lint/docs/` has no replacement index. --- ## 🟡 Minor - **`SentenceOpenerThereIs.yml:7` — first regex alternative is dead.** `(?:[;-]\s)There\s(is|are)` is fully subsumed by the `\bThere\s(is|are)\b` alternative: identical alert count, identical flagged sites. Its only effect is widening the captured span, which corrupts `%s` into `Don't start a sentence with '; There is'`. Also `[;-]` is ASCII hyphen — this repo's prose uses em dashes, so even the intended target is the wrong character. - **That rule doesn't implement "start a sentence."** No `^`, no `scope: sentence`; `ignorecase: false` is the only proxy for sentence-initial position. Over-matches headings, mid-sentence, table cells, link text; under-matches `there is` lowercase, `There's`, `There exist`. - **`VagueQualifier` and `SentenceOpenerThereIs` lint all YAML frontmatter keys.** They're the only two rules using bare `scope: text`; the other four are `text.frontmatter.description`. Verified a skill named `very-simple-skill` gets flagged on its `name:` line — unfixable without renaming the directory. No current name collides, but it also means the full 30-token list is now live against every skill's `description:`. - **The documented root cause is wrong about `|`.** `CONTEXT.md:75` and `vale-config/SKILL.md:25` both say the scope breaks for "a block scalar (`>`/`|`)". Only `>` (folded) breaks — bare vale matches every line of a `|` literal block. `vale-config` is shipped content agents treat as ground truth. - **`vale-config/SKILL.md:22`** asserts a fresh config "will fail or find nothing until `vale sync` runs" — this repo declares no `Packages` and works fine; `vale sync` is a no-op here. Scope the claim to package-based styles. - `skill-size-check.sh` boundary is inclusive (`>`), so exactly 500 lines passes where the source says "under 500"; `wc -l` undercounts a file with no trailing newline, disagreeing with `skill-audit/scripts/validate.sh:137` (`splitlines()`) at the boundary; nonexistent paths, directories, and zero args all exit 0 silently. - `lint-runner`'s genericization is a naming convention, not dispatch — no registry, no discovery, sample size of one. The `<linter>-config`/`<linter>-run` guard at `:24` is the substantive part and is good. But frontmatter `:4` still names `vale-config`/`vale-run` concretely, contradicting the `<linter>-*` body two lines down. - `lint-runner` is the repo's only agent pair with a non-1:1 tool mapping (`Grep`+`Glob` → single `search`). Plausibly correct, but it's the sole precedent and unverified against `field-inventory.md`. --- ## Test coverage Both new suites assert the shape of the implementation, not the spec. `test-vale-wrap.sh` — fixture deliberately includes a colon but no double quote, which is exactly why it passes while 26 real files silently fail. Uncovered: quotes/backslashes/unicode, blank line in a folded block, `--config=` form, zero args, absolute/nonexistent/subdirectory invocation, `|` and `>-`/`|+` variants, plain and quoted multi-line descriptions, exit-code fidelity. Test 3's line-preservation assertion (`MAX_LINE <= ORIG_LINES`) passes trivially if flattening drops all alerts. `test-skill-size-check.sh` — fixtures are 10 lines / 604 lines / ~6,004 words, all 20%+ from the threshold. Flipping `>` to `>=`, or changing `MAX_LINES` to 550, passes all three tests. No boundary pairs (500/501, 5000/5001), no zero-arg/missing-path/empty-file cases. Credit where due: `test-vale-wrap.sh` *is* a real regression test for the bug it was written for — neutering the flattening in a copy flips test 2 to FAIL. The gap is coverage, not construction. --- ## What genuinely holds - **Hook regex (round-1 finding 2) is exact.** Matches 45 files, excludes all 7 `docs/research/examples/**` files and `assets/templates/SKILL.md`, misses zero legitimate ones. - Folded-scalar flattening works for the unescaped case, `>-`, `description:` not first key, nested `metadata: description:` correctly left alone, `---` rules in body, CRLF, and line-number preservation. - `KyberforgeCopilot` scoping is correct and the round-1 rationale (item 1) is right: `Use proactively` is live in CC, dead in Copilot. Verified it fires on `.agent.md` and not `.md`. - Code blocks, inline code, link URLs, and HTML comments are correctly excluded from `scope: text`. - Token lists are properly word-boundary anchored — `simple` does not flag `simplify`, `various` does not flag `variation`. The separate `simple`/`simply` entries are each necessary. - The "28 violations" figure is accurate (parent commit: 28 alerts; HEAD: 0). ADR-0013 conforms to the repo's ADR format. File scope is unchanged as claimed. Rule schemas are valid; `%s` interpolation correct for both `tokens` and `raw`. - Manifest pair for `lint` agrees on name/version/paths; agent pair diff is a single line (the tools mapping); `agent-audit`'s own validators pass on it. --- **Recommendation: request changes.** Blocking 1 is the round-1 finding unfixed — same silent-clean signature, 58% of the corpus. Blocking 2 is a shipped skill that behaves differently now because a linter was wrong. Blocking 4 says the rule generating those edits is wrong ~100% of the time on this repo's voice, which makes the 28-violation cleanup a net negative on prose quality. I'd land 1 and 3 as fixes, revert the 2/4 prose edits wholesale, cut the token list down before re-running, and split the size-check into its own PR.
Defame1297 added 1 commit 2026-08-08 19:29:12 +00:00
Addresses PR #85's outstanding review items after grilling the open
questions against ADR-0013/CONTEXT.md/ADR-0010:

Blocking fixes:
- vale-wrap.sh: replace json.dumps() escaping (which silently defeated
  Vale's frontmatter scope on any description containing a quote,
  backslash, or non-ASCII char — ~58% of the corpus) with a single-quoted
  YAML scalar, substituting a Unicode right single quote for embedded
  apostrophes rather than '' doubling (Vale's frontmatter scanner isn't a
  full YAML parser and silently truncates on '' too).
- vale-wrap.sh: fix a blank-line-inside-a-folded-description truncation
  bug via indentation-based, blank-line-tolerant body capture; narrow
  flattening to `>`-style scalars only (`|` already works unflattened).
- skill-audit/agent-audit Step 1: make the vale-wrap.sh invocation
  cwd-independent via git rev-parse --show-toplevel, fixing a bug where
  no single cwd satisfied all three Step 1 commands.
- styles/Kyberforge/VagueQualifier.yml: prune 17 tokens verified
  false-positive-dominated on this repo's own voice via a real corpus
  sweep (obvious, clearly, usually, several, simple, easy, completely,
  simply, tiny, etc.), keep 13 with real or unattested noise. Revert the
  28 prose "fixes" those tokens drove across 14 skill files back to their
  original, correct wording, including a functional regression to
  caveman/SKILL.md's own filler-word list (a mention, not a use) — now
  guarded with vale-off comments against recurrence.

Gaps:
- --minAlertLevel=warning on the pre-commit hook and Step 1 invocation
  so warning-level rules actually surface, without collapsing the
  FAIL/SUGGESTION severity mapping skill-audit/agent-audit rely on.
- vale-wrap.sh: fix --config=<path> equals-form, absolute-path silent
  no-op, and a zero-file-argument stdin hang.
- Route vale-run and lint-runner through a documented wrapper script
  when a target repo has one, instead of unconditionally recommending
  bare `vale`.
- Wire Kyberforge.VagueQualifier/SentenceOpenerThereIs into skill-audit/
  agent-audit's dimension-mapping prose (Body discipline).
- Add plugins/lint/sources.md provenance for lint-runner (ADR-0010).
- Sync both marketplace.json lint-entry descriptions with plugin.json.
- Retune skill-size-check.sh's MAX_WORDS 5000->2900 (measured ~1.6-1.7
  tokens/word on this repo's corpus, the old value gated at ~8,500
  tokens against a stated 5,000 ceiling); fix the >/>= line-count
  boundary and wc -l undercount on files with no trailing newline.
- Document the vale binary as a Setup prerequisite in AGENTS.md.
- Fix SentenceOpenerThereIs's dead regex alternative and add a real
  sentence-start anchor/scope.
- Fix a stale docs/research/docs/vale/ index pointer in kyberforge's
  docs README (moved to plugins/lint/ in e1a5403).
- Rewrite ADR-0013's Consequences section past-tense to describe what
  actually landed, and record the styles-portability limitation
  (repo-root placement stays intentional; deferred to a separate
  session per this PR's review).

Test coverage: 9 new vale-wrap.sh fixtures (quotes, backslash/unicode,
blank-line paragraphs, --config= form, zero-arg/absolute-path handling,
literal-block no-regression) and boundary-pair tests for
skill-size-check.sh's line/word ceilings.

bash tests/run-tests.sh: 9 scripts + 125 bats assertions, all passing.
scripts/check-manifests.sh and claude plugin validate --strict: clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
Author
Collaborator

Pushed 55dc065, addressing everything open from both round-1 and round-2 review — grilled the open design questions against ADR-0013/CONTEXT.md/ADR-0010 first, then implemented in parallel across the affected plugins.

Blocking (round 2):

  • #1 escaping — vale-wrap.sh switched from json.dumps() to a single-quoted YAML scalar. One step further than the suggested fix: ''-doubling for embedded apostrophes turned out to have the same silent-truncation bug (Vale's frontmatter scanner isn't a full YAML parser), reproduced and confirmed. Fixed by substituting a Unicode right single quote for apostrophes in the scratch-only copy instead. New fixtures cover quotes, apostrophes, backslash+non-ASCII, and a blank line inside a folded description (which was also silently truncating the body capture — fixed via indentation-based parsing).
  • #2 caveman regression — reverted, plus the 27 other prose edits driven by tokens we've now pruned (see below), across 14 files in bin/git/gitea/kyberforge. Guarded caveman's filler-word line with vale off comments so a mention can't be mistaken for a use again.
  • #3 cwd bug — Step 1 in both audit skills now resolves vale-wrap.sh/.vale.ini via git rev-parse --show-toplevel, independent of caller cwd.
  • #4 token noise — re-ran the corpus sweep myself rather than adopting the suggested list as-is (it covered 25 of 30 tokens; I read contexts for the other 5 too). Landed on 17 cut / 13 kept — one addition worth flagging: simply had to be cut, since it's literally the word caveman's own filler-list quotes as a mention (missed by the original list, and load-bearing for reverting the caveman regression without immediately re-breaking it).

Gaps: --minAlertLevel=warning wired into the hook and Step 1 (severities left alone — raising them would've collapsed the FAIL/SUGGESTION mapping the audit skills rely on); --config= equals-form, absolute-path no-op, and zero-arg stdin hang all fixed in vale-wrap.sh; vale-run/lint-runner now prefer a documented wrapper over bare vale; both new rules wired into the audit skills' Body-discipline mapping; plugins/lint/sources.md added for lint-runner's provenance (ADR-0010); both marketplace.jsons synced to plugin.json's wording (+ patch bump, per marketplace-author's own convention); skill-size-check.sh's MAX_WORDS retuned 5000→2900 (measured this repo's actual chars/word ratio — the old value was gating at ~8,500 tokens against a stated 5,000 ceiling) plus its >/>= boundary and wc -l trailing-newline bugs; vale documented as an AGENTS.md Setup prerequisite; SentenceOpenerThereIs's dead regex branch and missing sentence-anchor fixed; stale docs/research/docs/vale/ pointer removed from kyberforge's docs README; ADR-0013's Consequences rewritten past-tense.

Deliberately deferred (not silently dropped): styles-portability (moving styles//.vale.ini into plugins/lint/ for external installs) stays repo-root for now — intentional per this ADR, tracked as a known limitation, revisiting in a separate session. Filing issues for this repo's other pre-commit linters (shellcheck etc.) is likewise out of scope for this PR.

Verification: bash tests/run-tests.sh — 9 scripts + 125 bats assertions, all passing (9 new vale-wrap.sh fixtures + boundary-pair tests for skill-size-check.sh added). scripts/check-manifests.sh and claude plugin validate --strict both clean. All of this ran through the actual pre-commit/pre-push hooks on the way in, including the newly-fixed vale-audit-prefilter and skill-size-check hooks linting their own fix.

🤖 Generated with Claude Code

Pushed `55dc065`, addressing everything open from both round-1 and round-2 review — grilled the open design questions against ADR-0013/CONTEXT.md/ADR-0010 first, then implemented in parallel across the affected plugins. **Blocking (round 2):** - **#1 escaping** — `vale-wrap.sh` switched from `json.dumps()` to a single-quoted YAML scalar. One step further than the suggested fix: `''`-doubling for embedded apostrophes turned out to have the *same* silent-truncation bug (Vale's frontmatter scanner isn't a full YAML parser), reproduced and confirmed. Fixed by substituting a Unicode right single quote for apostrophes in the scratch-only copy instead. New fixtures cover quotes, apostrophes, backslash+non-ASCII, and a blank line inside a folded description (which was also silently truncating the body capture — fixed via indentation-based parsing). - **#2 caveman regression** — reverted, plus the 27 other prose edits driven by tokens we've now pruned (see below), across 14 files in `bin`/`git`/`gitea`/`kyberforge`. Guarded caveman's filler-word line with `vale off` comments so a mention can't be mistaken for a use again. - **#3 cwd bug** — Step 1 in both audit skills now resolves `vale-wrap.sh`/`.vale.ini` via `git rev-parse --show-toplevel`, independent of caller cwd. - **#4 token noise** — re-ran the corpus sweep myself rather than adopting the suggested list as-is (it covered 25 of 30 tokens; I read contexts for the other 5 too). Landed on 17 cut / 13 kept — one addition worth flagging: **`simply`** had to be cut, since it's literally the word caveman's own filler-list quotes as a mention (missed by the original list, and load-bearing for reverting the caveman regression without immediately re-breaking it). **Gaps:** `--minAlertLevel=warning` wired into the hook and Step 1 (severities left alone — raising them would've collapsed the FAIL/SUGGESTION mapping the audit skills rely on); `--config=` equals-form, absolute-path no-op, and zero-arg stdin hang all fixed in `vale-wrap.sh`; `vale-run`/`lint-runner` now prefer a documented wrapper over bare `vale`; both new rules wired into the audit skills' Body-discipline mapping; `plugins/lint/sources.md` added for `lint-runner`'s provenance (ADR-0010); both `marketplace.json`s synced to `plugin.json`'s wording (+ patch bump, per `marketplace-author`'s own convention); `skill-size-check.sh`'s `MAX_WORDS` retuned 5000→2900 (measured this repo's actual chars/word ratio — the old value was gating at ~8,500 tokens against a stated 5,000 ceiling) plus its `>`/`>=` boundary and `wc -l` trailing-newline bugs; `vale` documented as an `AGENTS.md` Setup prerequisite; `SentenceOpenerThereIs`'s dead regex branch and missing sentence-anchor fixed; stale `docs/research/docs/vale/` pointer removed from kyberforge's docs README; ADR-0013's Consequences rewritten past-tense. **Deliberately deferred (not silently dropped):** styles-portability (moving `styles/`/`.vale.ini` into `plugins/lint/` for external installs) stays repo-root for now — intentional per this ADR, tracked as a known limitation, revisiting in a separate session. Filing issues for this repo's other pre-commit linters (shellcheck etc.) is likewise out of scope for this PR. Verification: `bash tests/run-tests.sh` — 9 scripts + 125 bats assertions, all passing (9 new `vale-wrap.sh` fixtures + boundary-pair tests for `skill-size-check.sh` added). `scripts/check-manifests.sh` and `claude plugin validate --strict` both clean. All of this ran through the actual pre-commit/pre-push hooks on the way in, including the newly-fixed `vale-audit-prefilter` and `skill-size-check` hooks linting their own fix. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Defame1297 added 1 commit 2026-08-08 19:40:21 +00:00
plugins/lint/docs/research/docs/vale/ had no top-level index pointing
into it, unlike plugins/kyberforge/docs/README.md which indexes its
own research directories. Add plugins/lint/docs/README.md mirroring
that convention: one line per file describing what it covers, plus a
provenance note tying the directory back to plugins/lint/sources.md
and the vale-config/vale-run skills that consume it.

Closes out a follow-up item from PR #85's review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
Author
Collaborator

Review round 3 (read-only — no edits made)

Verified empirically against the actual tree with vale 3.15.2 rather than reading the diff — ran the wrapper, the repo-wide sweep, and injected known-bad fixtures for each rule. Full test suite passes (9 scripts, 0 failed).

What the last commit did

6c0afb7 adds plugins/lint/docs/README.md — a 27-line index for the Vale research docs mirroring plugins/kyberforge/docs/README.md. Accurate against the actual directory contents, no issues.

The round-2 fixes hold up

Re-tested the things #1294/#1309 claim to have fixed; they genuinely work:

  • Multi-line folded descriptions now lint. A fixture with a 2-line description: > containing This skill, helps with, utilize, used for, assists with fired all five. The original blocking bug is dead.
  • All six rules fire (DescriptionOpener, VagueWording, PaddingPhrase, VagueQualifier, SentenceOpenerThereIs, KyberforgeCopilot.ProactivePhrase), and ProactivePhrase correctly does not fire on the CC .md file.
  • The tightened hook regex matches exactly the 45 real skill/agent files, excluding docs/research/examples/ and assets/templates/.
  • Repo-wide sweep is 0/0/0 across all 45 files — and this time that's real cleanliness, not the check silently not running.

🔴 Blocking: the "enforcing" hook doesn't enforce — Vale exits 0 on warnings

Five of the six rules are level: warning. Only DescriptionOpener is error. Verified:

$ scripts/vale-wrap.sh --config .vale.ini --minAlertLevel=warning <fixture>
 3:33  warning  Vague capability wording: 'helps with' ...   Kyberforge.VagueWording
 10:9  warning  'very' is vague filler wording ...           Kyberforge.VagueQualifier
✖ 0 errors, 2 warnings and 0 suggestions in 1 file.
EXIT=0

vale --help confirms the model: --no-exit Don't return a nonzero exit code **on errors**. --minAlertLevel controls display only, not the exit code.

Because pre-commit suppresses output from passing hooks, the net effect is worse than advisory: a commit introducing helps with, utilize, Use proactively, very, or There is produces no output at all and exits 0. The warnings are not just non-blocking, they're invisible.

This contradicts three places in this PR:

  • ADR-0013:53 — "Rules land directly in styles/Kyberforge, enforcing immediately" and :68 — "goes live in the blocking pre-commit hook immediately"
  • CONTEXT.md:77 — "rules land directly in styles/Kyberforge, blocking immediately, no trial tier"
  • Comment #1299 — "Fixed the 28 violations ... so the enforcing hook lands clean"

ADR-0013 explicitly rejected a report-only trial tier in favour of immediate enforcement. What actually shipped is a report-only tier whose reports nobody sees — the rejected option, arrived at by accident.

#1309 says severities were "left alone — raising them would've collapsed the FAIL/SUGGESTION mapping the audit skills rely on." Sound for the audit path, but it doesn't transfer to the hook path: the audit skills read Vale's severity strings, which are independent of the process exit code. You can have both. Options, in order of preference:

  1. Wrap the hook entry in a check that fails on any alert count > 0 (parse --output=line or --output=JSON), keeping severities intact for the audit mapping. Cleanest — decouples the two consumers.
  2. Raise the five rules to error and key the audit skills' mapping off rule identity rather than severity.
  3. Keep it advisory — but then fix ADR-0013 and CONTEXT.md to say so, and make the hook print its findings.

🔴 Blocking: vale-run's documented exit-code semantics are factually wrong

plugins/lint/skills/vale-run/SKILL.md, first Gotcha:

Vale exits non-zero whenever it finds an alert at or above MinAlertLevel — that's what makes it usable as a CI gate

Disproved by the run above: MinAlertLevel = suggestion, two warnings found, exit 0. This is shipped, installable guidance — an agent following it will build a CI gate that silently passes on everything except error-level alerts. It's the same wrong premise that produced the finding above, so I'd fix the skill and the hook together rather than as separate items.

The --no-exit row in the flag table ("Forces exit code 0 regardless of findings") inherits the same error, as does references/troubleshooting.md:53.

🔴 Blocking: 0 files reads as "clean" — the round-1 false-negative class, in a new place

.vale.ini's globs are plugins/*/skills/*/SKILL.md and plugins/*/agents/*.md. A skill anywhere else gets zero files scanned and a green checkmark:

$ scripts/vale-wrap.sh --config .vale.ini --minAlertLevel=warning .claude/skills/zzztest/SKILL.md
✔ 0 errors, 0 warnings and 0 suggestions in 0 files.
EXIT=0

The fixture contained This skill, helps with, very, and There is.

skill-audit/agent-audit are plugin skills — they audit skills in any repo, plus project-scope (.claude/skills/) and user-scope skills in this one. For every one of those, Step 1's Vale pass returns a clean bill of health, and the SKILL.md then instructs the auditor to "report them as findings without re-deriving by judgment" for the Description, Patterns, and Body-discipline sub-checks. Those checks get skipped in both directions.

The existing escape hatch — "Skip and fall back to judgment if vale or .vale.ini is unavailable" — doesn't catch this, because Vale is available and does succeed. It just scanned nothing.

This is the deferred styles-portability item's blast radius, and it's larger than ADR-0013:81-87 frames it. Deferring the fix (moving styles/ into plugins/lint/) is defensible; deferring the guard isn't. Minimum viable fix in this PR: have Step 1 assert Vale reported ≥1 file scanned, and fall back to Step 2/3 judgment when it didn't.

🟠 The documented Markdown suppression syntax doesn't work

vale-run/SKILL.md Gotcha 3 and references/troubleshooting.md:10-15 both give {/* vale off */} as the "Markdown/MDX" syntax. Tested side by side in one .md file:

Form Result
{/* vale Kyberforge.VagueQualifier = NO */} still flagged
<!-- vale Kyberforge.VagueQualifier = NO --> suppressed correctly

{/* */} is MDX-only. This repo's own fix — the caveman guard in plugins/bin/skills/caveman/SKILL.md — correctly uses <!-- -->, so the shipped skill contradicts the working practice introduced in the same PR. An agent following vale-run would add a suppression that silently does nothing, which is how the caveman regression happened in the first place.

🟠 The branch doesn't merge

Gitea reports mergeable: false. One real conflict, in AGENTS.md: main's #86 rewrote the Setup section, and this branch's vale-binary prerequisite lands inside the removed block. Worth flagging because a naive resolution loses content in both directions — main's rewrite already references lint:vale-config / lint:vale-run, skills that only exist on this branch, while dropping the vale-install prerequisite that makes them usable. Resolution needs the prerequisite re-homed into main's new "Setup and testing" section, not either side taken wholesale. CONTEXT.md also changed on both sides but merges cleanly.

🟠 vale-wrap.sh isn't the drop-in replacement its docstring claims

Lines 18-39 resolve every relative --config path against repo_root, not the caller's cwd:

$ cd plugins/lint
$ vale --config ../../.vale.ini <file>                          # works, exit 0
$ ../../scripts/vale-wrap.sh --config ../../.vale.ini <file>
path '/root/ai-development/../../.vale.ini' does not exist       # exit 2

The header comment says "Drop-in replacement for calling vale directly: same args, same exit code," and both audit SKILL.mds tell agents it "runs correctly regardless of the caller's cwd." Both are true only for repo-root-relative or absolute paths — an undocumented invented convention. tests/test-vale-wrap.sh:214-215 blesses it by only ever testing the root-relative form from a subdirectory.

Low real-world impact (both call sites use absolute paths, and it fails loudly), but the docstring and the two SKILL.md sentences overstate the contract. Either resolve relative paths against $PWD — the actually-drop-in behaviour — or document the convention as a constraint.

🟡 Minor

  • skill-audit/SKILL.md:70 documents Kyberforge.VagueQualifier as flagging "vague filler like 'clearly', 'obviously'". Neither token is in styles/Kyberforge/VagueQualifier.yml — the list is easily, everyone knows, exceedingly, excellent, extremely, huge, interestingly, of course, quite, remarkably, surprisingly, vast, very. Pick real examples from the list, or add those two tokens.
  • Off-by-one between the two size ceilings. scripts/skill-size-check.sh:29 fails at lines >= 500; skill-audit/scripts/validate.sh:138 passes at line_count <= 500. A 500-line SKILL.md passes its own audit and is blocked by the commit hook. Pick one boundary. (The MAX_WORDS=2900 retune is fine — largest file in the corpus is 2,489 words / 292 lines, real headroom.)
  • AGENTS.md: "then run vale sync to pull the styles declared in .vale.ini." There's no Packages key — vale sync reports Synced 0 package(s). Kyberforge/KyberforgeCopilot are committed directories. Harmless but misleading; drop it or say the styles are vendored.

Also checked, no issues

plugins/lint/{plugin.json,.claude-plugin/plugin.json,.mcp.json,hooks.json} match the established per-plugin pattern (empty {"mcpServers":{}}/{"hooks":{}} stubs are the repo convention, not omissions); both marketplace.jsons are in sync at 0.3.1 with matching descriptions; sources.md provenance satisfies ADR-0010; ADR-0013's structure matches ADR-0010's; the lint-runner pair is consistent across providers with correct search → Grep, Glob mapping; the 27 prose edits across bin/gitea/kyberforge are genuine improvements, not token-churn.


Recommendation: the three 🔴 items share one root cause — an incorrect model of Vale's exit-code and file-matching behaviour — so they're one focused fix, not three. Hold the merge for those plus the AGENTS.md conflict resolution; the 🟠/🟡 items are safe to batch into the same pass.

## Review round 3 (read-only — no edits made) Verified empirically against the actual tree with `vale` 3.15.2 rather than reading the diff — ran the wrapper, the repo-wide sweep, and injected known-bad fixtures for each rule. Full test suite passes (9 scripts, 0 failed). ### What the last commit did `6c0afb7` adds `plugins/lint/docs/README.md` — a 27-line index for the Vale research docs mirroring `plugins/kyberforge/docs/README.md`. Accurate against the actual directory contents, no issues. ### The round-2 fixes hold up Re-tested the things #1294/#1309 claim to have fixed; they genuinely work: - **Multi-line folded descriptions now lint.** A fixture with a 2-line `description: >` containing `This skill`, `helps with`, `utilize`, `used for`, `assists with` fired all five. The original blocking bug is dead. - All six rules fire (`DescriptionOpener`, `VagueWording`, `PaddingPhrase`, `VagueQualifier`, `SentenceOpenerThereIs`, `KyberforgeCopilot.ProactivePhrase`), and `ProactivePhrase` correctly does **not** fire on the CC `.md` file. - The tightened hook regex matches exactly the 45 real skill/agent files, excluding `docs/research/examples/` and `assets/templates/`. - Repo-wide sweep is 0/0/0 across all 45 files — and this time that's real cleanliness, not the check silently not running. --- ## 🔴 Blocking: the "enforcing" hook doesn't enforce — Vale exits 0 on warnings Five of the six rules are `level: warning`. Only `DescriptionOpener` is `error`. Verified: ``` $ scripts/vale-wrap.sh --config .vale.ini --minAlertLevel=warning <fixture> 3:33 warning Vague capability wording: 'helps with' ... Kyberforge.VagueWording 10:9 warning 'very' is vague filler wording ... Kyberforge.VagueQualifier ✖ 0 errors, 2 warnings and 0 suggestions in 1 file. EXIT=0 ``` `vale --help` confirms the model: `--no-exit Don't return a nonzero exit code **on errors**.` `--minAlertLevel` controls *display only*, not the exit code. Because pre-commit suppresses output from passing hooks, the net effect is worse than advisory: a commit introducing `helps with`, `utilize`, `Use proactively`, `very`, or `There is` produces **no output at all** and exits 0. The warnings are not just non-blocking, they're invisible. This contradicts three places in this PR: - ADR-0013:53 — "**Rules land directly in `styles/Kyberforge`, enforcing immediately**" and :68 — "goes live in the blocking pre-commit hook immediately" - `CONTEXT.md:77` — "rules land directly in `styles/Kyberforge`, blocking immediately, no trial tier" - Comment #1299 — "Fixed the 28 violations ... **so the enforcing hook lands clean**" ADR-0013 explicitly *rejected* a report-only trial tier in favour of immediate enforcement. What actually shipped is a report-only tier whose reports nobody sees — the rejected option, arrived at by accident. #1309 says severities were "left alone — raising them would've collapsed the FAIL/SUGGESTION mapping the audit skills rely on." Sound for the audit path, but it doesn't transfer to the hook path: the audit skills read Vale's *severity strings*, which are independent of the process exit code. You can have both. Options, in order of preference: 1. Wrap the hook entry in a check that fails on any alert count > 0 (parse `--output=line` or `--output=JSON`), keeping severities intact for the audit mapping. Cleanest — decouples the two consumers. 2. Raise the five rules to `error` and key the audit skills' mapping off rule identity rather than severity. 3. Keep it advisory — but then fix ADR-0013 and CONTEXT.md to say so, and make the hook print its findings. ## 🔴 Blocking: `vale-run`'s documented exit-code semantics are factually wrong `plugins/lint/skills/vale-run/SKILL.md`, first Gotcha: > Vale exits non-zero whenever it finds an alert at or above `MinAlertLevel` — that's what makes it usable as a CI gate Disproved by the run above: `MinAlertLevel = suggestion`, two warnings found, exit 0. This is shipped, installable guidance — an agent following it will build a CI gate that silently passes on everything except `error`-level alerts. It's the same wrong premise that produced the finding above, so I'd fix the skill and the hook together rather than as separate items. The `--no-exit` row in the flag table ("Forces exit code `0` regardless of findings") inherits the same error, as does `references/troubleshooting.md:53`. ## 🔴 Blocking: `0 files` reads as "clean" — the round-1 false-negative class, in a new place `.vale.ini`'s globs are `plugins/*/skills/*/SKILL.md` and `plugins/*/agents/*.md`. A skill anywhere else gets zero files scanned and a green checkmark: ``` $ scripts/vale-wrap.sh --config .vale.ini --minAlertLevel=warning .claude/skills/zzztest/SKILL.md ✔ 0 errors, 0 warnings and 0 suggestions in 0 files. EXIT=0 ``` The fixture contained `This skill`, `helps with`, `very`, and `There is`. `skill-audit`/`agent-audit` are plugin skills — they audit skills in *any* repo, plus project-scope (`.claude/skills/`) and user-scope skills in this one. For every one of those, Step 1's Vale pass returns a clean bill of health, and the SKILL.md then instructs the auditor to "report them as findings without re-deriving by judgment" for the Description, Patterns, and Body-discipline sub-checks. Those checks get skipped in both directions. The existing escape hatch — "Skip and fall back to judgment if vale or `.vale.ini` is unavailable" — doesn't catch this, because Vale *is* available and *does* succeed. It just scanned nothing. This is the deferred styles-portability item's blast radius, and it's larger than ADR-0013:81-87 frames it. Deferring the *fix* (moving `styles/` into `plugins/lint/`) is defensible; deferring the *guard* isn't. Minimum viable fix in this PR: have Step 1 assert Vale reported ≥1 file scanned, and fall back to Step 2/3 judgment when it didn't. ## 🟠 The documented Markdown suppression syntax doesn't work `vale-run/SKILL.md` Gotcha 3 and `references/troubleshooting.md:10-15` both give `{/* vale off */}` as the "Markdown/MDX" syntax. Tested side by side in one `.md` file: | Form | Result | |---|---| | `{/* vale Kyberforge.VagueQualifier = NO */}` | **still flagged** | | `<!-- vale Kyberforge.VagueQualifier = NO -->` | suppressed correctly | `{/* */}` is MDX-only. This repo's own fix — the caveman guard in `plugins/bin/skills/caveman/SKILL.md` — correctly uses `<!-- -->`, so the shipped skill contradicts the working practice introduced in the same PR. An agent following `vale-run` would add a suppression that silently does nothing, which is how the caveman regression happened in the first place. ## 🟠 The branch doesn't merge Gitea reports `mergeable: false`. One real conflict, in `AGENTS.md`: main's #86 rewrote the Setup section, and this branch's vale-binary prerequisite lands inside the removed block. Worth flagging because a naive resolution loses content in both directions — main's rewrite **already references `lint:vale-config` / `lint:vale-run`**, skills that only exist on this branch, while dropping the vale-install prerequisite that makes them usable. Resolution needs the prerequisite re-homed into main's new "Setup and testing" section, not either side taken wholesale. `CONTEXT.md` also changed on both sides but merges cleanly. ## 🟠 `vale-wrap.sh` isn't the drop-in replacement its docstring claims Lines 18-39 resolve *every* relative `--config` path against `repo_root`, not the caller's cwd: ``` $ cd plugins/lint $ vale --config ../../.vale.ini <file> # works, exit 0 $ ../../scripts/vale-wrap.sh --config ../../.vale.ini <file> path '/root/ai-development/../../.vale.ini' does not exist # exit 2 ``` The header comment says "Drop-in replacement for calling `vale` directly: same args, same exit code," and both audit SKILL.mds tell agents it "runs correctly regardless of the caller's cwd." Both are true only for repo-root-relative or absolute paths — an undocumented invented convention. `tests/test-vale-wrap.sh:214-215` blesses it by only ever testing the root-relative form from a subdirectory. Low real-world impact (both call sites use absolute paths, and it fails loudly), but the docstring and the two SKILL.md sentences overstate the contract. Either resolve relative paths against `$PWD` — the actually-drop-in behaviour — or document the convention as a constraint. ## 🟡 Minor - **`skill-audit/SKILL.md:70`** documents `Kyberforge.VagueQualifier` as flagging *"vague filler like 'clearly', 'obviously'"*. Neither token is in `styles/Kyberforge/VagueQualifier.yml` — the list is `easily, everyone knows, exceedingly, excellent, extremely, huge, interestingly, of course, quite, remarkably, surprisingly, vast, very`. Pick real examples from the list, or add those two tokens. - **Off-by-one between the two size ceilings.** `scripts/skill-size-check.sh:29` fails at `lines >= 500`; `skill-audit/scripts/validate.sh:138` passes at `line_count <= 500`. A 500-line SKILL.md passes its own audit and is blocked by the commit hook. Pick one boundary. (The `MAX_WORDS=2900` retune is fine — largest file in the corpus is 2,489 words / 292 lines, real headroom.) - **`AGENTS.md`**: "then run `vale sync` to pull the styles declared in `.vale.ini`." There's no `Packages` key — `vale sync` reports `Synced 0 package(s)`. `Kyberforge`/`KyberforgeCopilot` are committed directories. Harmless but misleading; drop it or say the styles are vendored. ## Also checked, no issues `plugins/lint/{plugin.json,.claude-plugin/plugin.json,.mcp.json,hooks.json}` match the established per-plugin pattern (empty `{"mcpServers":{}}`/`{"hooks":{}}` stubs are the repo convention, not omissions); both `marketplace.json`s are in sync at `0.3.1` with matching descriptions; `sources.md` provenance satisfies ADR-0010; ADR-0013's structure matches ADR-0010's; the `lint-runner` pair is consistent across providers with correct `search` → `Grep, Glob` mapping; the 27 prose edits across `bin`/`gitea`/`kyberforge` are genuine improvements, not token-churn. --- **Recommendation:** the three 🔴 items share one root cause — an incorrect model of Vale's exit-code and file-matching behaviour — so they're one focused fix, not three. Hold the merge for those plus the `AGENTS.md` conflict resolution; the 🟠/🟡 items are safe to batch into the same pass.
Defame1297 force-pushed feat/84-vale-audit-prefilter from 6c0afb7c81 to aff5b6c4c8 2026-08-08 20:47:31 +00:00 Compare
Author
Collaborator

Pushed 149d564 + aff5b6c, resolving everything from round-3 review. Grilled the open design questions first — the outcome changed two of my own recommendations, noted below rather than quietly swapped.

This branch was rebased onto main (which had moved on via #86) rather than merged, so every pre-existing SHA reference in this thread now points at a commit that is no longer on the branch. Mapping, oldest last:

Cited in Old SHA New SHA
#1294 0bbb965 bbb0dcd
#1309 55dc065 792d3e1
#1316 6c0afb7 210b192

Full remap for the rest of the branch: 2eb13f7→3324a73, 28058d3→544392b, 7118ace→8d56290, 75f65d8→cbc33d9, e1a5403→f326df4, 4d6f313→57bdfa9, 0c0d51f→59ad2a3, cb2257d→8b00728, e4abe23→d1afdbe, 88888c4→5e22672. Subjects are unchanged, so the mapping is 1:1 by commit message. The same table is now in the PR description so it survives thread scrolling.

The AGENTS.md conflict is resolved and the PR is mergeable again. Resolution needed care in both directions: main's rewrite had already added lint:vale-config/lint:vale-run references while dropping the vale-install prerequisite, so the prerequisite was re-homed into main's new "Setup and testing" section rather than either side being taken wholesale.

🔴 #1 — the gate didn't gate. Fixed, and the fix is smaller than proposed

Confirmed exactly as reported. The fix went a different way than my own suggestion: rather than wrapping the hook to count alerts, every rule is now level: error, so Vale's own exit code is correct and the hook entry collapses to a bare scripts/vale-wrap.sh --config .vale.ini — no wrapper-around-the-wrapper, no JSON parsing, no jq.

That means the graded error→FAIL / warning→SUGGESTION mapping is gone: every Vale alert is a FAIL, in the commit gate and the audit alike. #1309's objection — that raising severities would collapse the mapping the audit skills rely on — is real, but the code says the dependency is skill-author's must-fix gate (SKILL.md:223, :288), not report cosmetics. Making prose violations must-fix is the intended behaviour, and it matches every other gate here: shellcheck, the test suite, and conventional-pre-commit all have no ignorable tier. One rule set, one verdict, nothing to keep in sync.

ADR-0013 now records why graded severities cannot gate, so this can't be reintroduced by accident. Its "Considered options" also notes that shipping graded severities accidentally recreated the report-only trial tier that same section had rejected.

🔴 #2 — vale-run's exit-code model. Fixed, plus a third instance

Corrected in vale-run/SKILL.md, its --no-exit and --minAlertLevel flag rows, and — not in the original report — a third copy of the same false claim in references/troubleshooting.md's "CI failing unexpectedly" section, which would have re-propagated it.

Also fixed alongside: the documented Markdown suppression syntax. {/* vale off */} is MDX-only and does nothing in a plain .md file — verified side by side; the alert still fires. Markdown needs <!-- vale off -->. Both the skill and the reference now show the two forms separately.

🔴 #3 — 0 files reads as clean. Fixed at the root, not with a guard

Investigating this turned up something better than the proposed guard: .vale.ini's globs were scoping nothing at all. Vale's * crosses /, so [plugins/*/agents/*.md] already matched plugins/*/docs/research/examples/**/agents/*.md, and [plugins/*/skills/*/SKILL.md] already matched assets/templates/SKILL.md — the two paths CONTEXT.md claimed they excluded. Verified against fixtures. All real scoping is, and always was, the pre-commit hook's files: regex.

agent-audit's own Gotchas already say plugin scope is detected by plugin.json presence, "not by the file path pattern" — the Vale step was violating that. So the globs are now path-agnostic ([**/SKILL.md], [**/agents/*.md], [**/*.agent.md]), which makes the prefilter work on project-scope and other-repo skills instead of silently reporting them clean. The hook's files: regex is untouched and still does all the scoping. The 0-files guard is in both audit skills too, as defence-in-depth: a zero-file run is now NOT RUN, not clean.

🟠 VagueQualifier deleted — measured, not assumed

Not in the original report; it came out of grilling whether the rule earns a blocking severity. Measured against the 41 skill/agent files as they stood before the rule ever ran:

Rule In-corpus hits Verdict
VagueQualifier 2 1 marginal, 1 false positive
SentenceOpenerThereIs 2 both clean rewrites

The false positive is caveman/SKILL.md, which quotes "of course" as an example of filler — a mention, not a use, unfixable by rewriting. It forced the only Vale suppression comments in the repo, two of which were dead anyway (they suppressed VagueWording, a frontmatter-scoped rule, on a body line). On 273 held-out markdown files it fired 15 times: 9 inside out-of-scope docs/research/examples/, and the remaining 6 all the word "very" in two idioms in a single research doc, each already sitting next to the hard number carrying the fact.

One marginal catch per 41 files doesn't pay for a permanent suppression comment, so the rule is gone and all four suppression lines with it — the repo is back to zero suppressions. SentenceOpenerThereIs survives on its own evidence (22 held-out hits, no suppressions needed).

New house convention recorded in CONTEXT.md and vale-run: banned phrasing that must be mentioned rather than used goes in backticks or a fenced code block — verified that Vale skips code spans and fences, so no suppression is needed at all. Inline <!-- vale Rule = NO --> is the fallback only where backticking is impossible.

🟠 / 🟡 remainder

  • vale-wrap.sh now resolves relative --config values and relative file arguments against the caller's cwd, as vale does. The file-argument half was worse than the --config half I reported: a relative path that didn't resolve from the repo root fell through and exec'd bare vale, silently skipping the frontmatter flattening the wrapper exists for. Absolute paths inside the cwd are relativized so reports cite resolvable paths rather than scratch ones.
  • skill-size-check.sh fails only above 500 lines, agreeing with validate.sh's <= 500.
  • VagueQualifier doc mismatch ("clearly"/"obviously", neither of which was ever in the token list) removed along with the rule.
  • AGENTS.md's vale sync instruction dropped — there is no Packages key, so it syncs 0 packages; the styles are committed under styles/.
  • Patch-bumped bin 1.1.1, kyberforge 1.2.4, lint 1.1.3. Shipped skill content changed in all three without a manifest change, so installed copies would have kept serving from cache. It matters most for lint: anyone at 1.1.2 has a cached vale-run/SKILL.md telling them Vale exits non-zero on warnings.

Still deferred

Styles portability — moving styles/ and .vale.ini into plugins/lint/ so the prefilter travels to repos that install kyberforge@holocron externally. Unchanged from #1309 and still recorded as a known limitation in ADR-0013. The path-agnostic globs above reduce its urgency (the config now works wherever it's pointed) but don't remove it: an external install still has no .vale.ini to point at.

Verification

bash tests/run-tests.sh — 9 scripts, 0 failed; the vale-wrap.sh sub-suite is 15 cases, including three new ones for the cwd-relative --config and file-argument regressions. Those three were checked for non-vacuity by running them against the previous script: they fail there and pass here.

pre-commit run --all-files passes for vale-audit-prefilter, skill-size-check and shellcheck; scripts/check-manifests.sh and claude plugin validate --strict clean on every plugin. All of it went in through the real pre-commit and pre-push hooks, including the newly-strict Vale gate linting its own fix.

One pre-existing breakage surfaced and was fixed rather than worked around: tests/test-vale-wrap.sh broke the moment rules became error, because its pipefail assertions inverted once Vale started exiting 1. Fixed with a helper that separates alert-text assertions from exit-code assertions.

🤖 Generated with Claude Code

Pushed `149d564` + `aff5b6c`, resolving everything from round-3 review. Grilled the open design questions first — the outcome changed two of my own recommendations, noted below rather than quietly swapped. ## ⚠️ Branch was rebased — old commit links are dead This branch was rebased onto `main` (which had moved on via #86) rather than merged, so every pre-existing SHA reference in this thread now points at a commit that is no longer on the branch. Mapping, oldest last: | Cited in | Old SHA | New SHA | |---|---|---| | [#1294](#issuecomment-1294) | `0bbb965` | `bbb0dcd` | | [#1309](#issuecomment-1309) | `55dc065` | `792d3e1` | | [#1316](#issuecomment-1316) | `6c0afb7` | `210b192` | Full remap for the rest of the branch: `2eb13f7`→`3324a73`, `28058d3`→`544392b`, `7118ace`→`8d56290`, `75f65d8`→`cbc33d9`, `e1a5403`→`f326df4`, `4d6f313`→`57bdfa9`, `0c0d51f`→`59ad2a3`, `cb2257d`→`8b00728`, `e4abe23`→`d1afdbe`, `88888c4`→`5e22672`. Subjects are unchanged, so the mapping is 1:1 by commit message. The same table is now in the PR description so it survives thread scrolling. The `AGENTS.md` conflict is resolved and the PR is `mergeable` again. Resolution needed care in both directions: `main`'s rewrite had already added `lint:vale-config`/`lint:vale-run` references while dropping the vale-install prerequisite, so the prerequisite was re-homed into main's new "Setup and testing" section rather than either side being taken wholesale. ## 🔴 #1 — the gate didn't gate. Fixed, and the fix is smaller than proposed Confirmed exactly as reported. The fix went a different way than my own suggestion: rather than wrapping the hook to count alerts, **every rule is now `level: error`**, so Vale's own exit code is correct and the hook entry collapses to a bare `scripts/vale-wrap.sh --config .vale.ini` — no wrapper-around-the-wrapper, no JSON parsing, no `jq`. That means the graded `error`→FAIL / `warning`→SUGGESTION mapping is gone: **every Vale alert is a FAIL**, in the commit gate and the audit alike. #1309's objection — that raising severities would collapse the mapping the audit skills rely on — is real, but the code says the dependency is `skill-author`'s must-fix gate (`SKILL.md:223`, `:288`), not report cosmetics. Making prose violations must-fix is the intended behaviour, and it matches every other gate here: shellcheck, the test suite, and conventional-pre-commit all have no ignorable tier. One rule set, one verdict, nothing to keep in sync. ADR-0013 now records *why* graded severities cannot gate, so this can't be reintroduced by accident. Its "Considered options" also notes that shipping graded severities accidentally recreated the report-only trial tier that same section had rejected. ## 🔴 #2 — `vale-run`'s exit-code model. Fixed, plus a third instance Corrected in `vale-run/SKILL.md`, its `--no-exit` and `--minAlertLevel` flag rows, and — not in the original report — a **third copy of the same false claim** in `references/troubleshooting.md`'s "CI failing unexpectedly" section, which would have re-propagated it. Also fixed alongside: the documented Markdown suppression syntax. `{/* vale off */}` is MDX-only and does **nothing** in a plain `.md` file — verified side by side; the alert still fires. Markdown needs `<!-- vale off -->`. Both the skill and the reference now show the two forms separately. ## 🔴 #3 — `0 files` reads as clean. Fixed at the root, not with a guard Investigating this turned up something better than the proposed guard: **`.vale.ini`'s globs were scoping nothing at all.** Vale's `*` crosses `/`, so `[plugins/*/agents/*.md]` already matched `plugins/*/docs/research/examples/**/agents/*.md`, and `[plugins/*/skills/*/SKILL.md]` already matched `assets/templates/SKILL.md` — the two paths CONTEXT.md claimed they excluded. Verified against fixtures. All real scoping is, and always was, the pre-commit hook's `files:` regex. `agent-audit`'s own Gotchas already say plugin scope is detected by `plugin.json` presence, "**not by the file path pattern**" — the Vale step was violating that. So the globs are now path-agnostic (`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]`), which makes the prefilter work on project-scope and other-repo skills instead of silently reporting them clean. The hook's `files:` regex is untouched and still does all the scoping. The 0-files guard is in both audit skills too, as defence-in-depth: a zero-file run is now NOT RUN, not clean. ## 🟠 `VagueQualifier` deleted — measured, not assumed Not in the original report; it came out of grilling whether the rule earns a blocking severity. Measured against the 41 skill/agent files as they stood *before* the rule ever ran: | Rule | In-corpus hits | Verdict | |---|---|---| | `VagueQualifier` | 2 | 1 marginal, 1 false positive | | `SentenceOpenerThereIs` | 2 | both clean rewrites | The false positive is `caveman/SKILL.md`, which *quotes* "of course" as an example of filler — a mention, not a use, unfixable by rewriting. It forced the only Vale suppression comments in the repo, two of which were dead anyway (they suppressed `VagueWording`, a frontmatter-scoped rule, on a body line). On 273 held-out markdown files it fired 15 times: 9 inside out-of-scope `docs/research/examples/`, and the remaining 6 all the word "very" in two idioms in a single research doc, each already sitting next to the hard number carrying the fact. One marginal catch per 41 files doesn't pay for a permanent suppression comment, so the rule is gone and all four suppression lines with it — **the repo is back to zero suppressions**. `SentenceOpenerThereIs` survives on its own evidence (22 held-out hits, no suppressions needed). New house convention recorded in CONTEXT.md and `vale-run`: banned phrasing that must be *mentioned* rather than *used* goes in backticks or a fenced code block — verified that Vale skips code spans and fences, so no suppression is needed at all. Inline `<!-- vale Rule = NO -->` is the fallback only where backticking is impossible. ## 🟠 / 🟡 remainder - **`vale-wrap.sh`** now resolves relative `--config` values *and* relative file arguments against the caller's cwd, as `vale` does. The file-argument half was worse than the `--config` half I reported: a relative path that didn't resolve from the repo root fell through and `exec`'d bare `vale`, silently skipping the frontmatter flattening the wrapper exists for. Absolute paths inside the cwd are relativized so reports cite resolvable paths rather than scratch ones. - **`skill-size-check.sh`** fails only *above* 500 lines, agreeing with `validate.sh`'s `<= 500`. - **`VagueQualifier` doc mismatch** ("clearly"/"obviously", neither of which was ever in the token list) removed along with the rule. - **`AGENTS.md`'s `vale sync` instruction** dropped — there is no `Packages` key, so it syncs 0 packages; the styles are committed under `styles/`. - **Patch-bumped `bin` 1.1.1, `kyberforge` 1.2.4, `lint` 1.1.3.** Shipped skill content changed in all three without a manifest change, so installed copies would have kept serving from cache. It matters most for `lint`: anyone at 1.1.2 has a cached `vale-run/SKILL.md` telling them Vale exits non-zero on warnings. ## Still deferred Styles portability — moving `styles/` and `.vale.ini` into `plugins/lint/` so the prefilter travels to repos that install `kyberforge@holocron` externally. Unchanged from #1309 and still recorded as a known limitation in ADR-0013. The path-agnostic globs above reduce its urgency (the config now *works* wherever it's pointed) but don't remove it: an external install still has no `.vale.ini` to point at. ## Verification `bash tests/run-tests.sh` — 9 scripts, 0 failed; the `vale-wrap.sh` sub-suite is 15 cases, including three new ones for the cwd-relative `--config` and file-argument regressions. Those three were checked for non-vacuity by running them against the previous script: they fail there and pass here. `pre-commit run --all-files` passes for `vale-audit-prefilter`, `skill-size-check` and `shellcheck`; `scripts/check-manifests.sh` and `claude plugin validate --strict` clean on every plugin. All of it went in through the real pre-commit and pre-push hooks, including the newly-strict Vale gate linting its own fix. One pre-existing breakage surfaced and was fixed rather than worked around: `tests/test-vale-wrap.sh` broke the moment rules became `error`, because its `pipefail` assertions inverted once Vale started exiting 1. Fixed with a helper that separates alert-text assertions from exit-code assertions. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Defame1297 added 1 commit 2026-08-08 20:56:23 +00:00
Three rounds of review on the Vale prefilter surfaced patterns worth
keeping rather than just fixing.

The first has now recurred three times in a single PR — a check reporting
success because it had silently not run — so it is flagged as a
graduation candidate per LESSONS.md's own three-instance rule.

- A clean linter result can mean "nothing was checked": the frontmatter
  scope silently not matching, warning-level rules never affecting an
  exit code, and globs matching zero files all produced green results
  that were then cited as evidence of cleanliness.
- One signal, two consumers, no named distinction: Vale severities were
  tuned for the audit report while the commit gate silently inherited the
  resulting exit code, because CONTEXT.md described both as one mechanism.
- Measure a rule's false-positive rate at the severity you will ship it
  at: VagueQualifier was trialled at warning, where a false positive is
  free, and shipped at error, where it costs a blocked commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
Defame1297 added 1 commit 2026-08-09 10:07:05 +00:00
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
Defame1297 added 1 commit 2026-08-09 10:22:50 +00:00
.pre-commit-hooks.yaml now exposes hooks to external consumers pinning
rev: <tag>, but nothing enforced that a tag actually gets cut when the
files it references change — relying on memory is exactly what this
repo's governance rules say to avoid for a repeatable, deterministic
check.

scripts/check-release-needed.sh hard-fails at pre-push, but only when
PRE_COMMIT_REMOTE_BRANCH (set by pre-commit's hook-impl) is
refs/heads/main: it diffs .pre-commit-hooks.yaml's referenced paths
against the last tag reachable from HEAD, and fails if either no tag
exists yet or something changed since. It's a silent no-op on every
other branch — hard-failing on feature-branch pushes mid-review would
force a premature tag on a commit that might not survive a
squash-merge, the exact risk the repo: local (vs. pinned self-
reference) decision in ADR-0014 already avoids for this repo's own
dev-time gate.

Verified against the real git pre-push hook path (not just the script
in isolation): simulated stdin matching git's pre-push protocol through
.git/hooks/pre-push, confirmed it correctly fires and fails when
targeting main with no tag, and is silent otherwise.

ADR: 0014
Refs: #87
Author
Collaborator

Added two follow-up commits addressing the styles-portability limitation ADR-0013 deliberately deferred:

1164f3a — make Vale prefilter portable via the plugin
skill-audit/agent-audit resolved vale-wrap.sh/.vale.ini via git rev-parse --show-toplevel, which only works inside this repo — installed as an external plugin, the prefilter silently fell back to full LLM judgment. Vale's config/styles/wrapper now ship inside the plugin itself (plugins/kyberforge/skills/agent-audit/assets/vale/ canonical, plugins/kyberforge/skills/skill-audit/assets/vale/ subset). A new root .pre-commit-hooks.yaml lets external repos enforce the same rules via git hooks/CI, independent of Claude Code entirely. This repo's own pre-commit hook now dogfoods the same plugin-bundled copies (repo: local, split into -skill/-agent hooks after confirming a combined hook would silently 0-file-skip one file type). scripts/check-vale-style-sync.sh guards the two copies against drift.

4d018af — hard-fail on main when a release tag is needed
External consumers pin rev: <tag>, so scripts/check-release-needed.sh now hard-fails at pre-push, but only when pushing to main — it checks whether files exposed via .pre-commit-hooks.yaml changed since the last tag (or no tag exists yet) and fails if so. Silent no-op on feature branches, so it never forces a premature tag on a commit that might not survive a squash-merge.

Full details in docs/adr/0014-vale-prefilter-ships-from-the-plugin.md. First release tag is intentionally deferred until this merges to main — tracked in #87.

Added two follow-up commits addressing the styles-portability limitation ADR-0013 deliberately deferred: **`1164f3a` — make Vale prefilter portable via the plugin** `skill-audit`/`agent-audit` resolved `vale-wrap.sh`/`.vale.ini` via `git rev-parse --show-toplevel`, which only works inside this repo — installed as an external plugin, the prefilter silently fell back to full LLM judgment. Vale's config/styles/wrapper now ship inside the plugin itself (`plugins/kyberforge/skills/agent-audit/assets/vale/` canonical, `plugins/kyberforge/skills/skill-audit/assets/vale/` subset). A new root `.pre-commit-hooks.yaml` lets external repos enforce the same rules via git hooks/CI, independent of Claude Code entirely. This repo's own pre-commit hook now dogfoods the same plugin-bundled copies (`repo: local`, split into `-skill`/`-agent` hooks after confirming a combined hook would silently 0-file-skip one file type). `scripts/check-vale-style-sync.sh` guards the two copies against drift. **`4d018af` — hard-fail on main when a release tag is needed** External consumers pin `rev: <tag>`, so `scripts/check-release-needed.sh` now hard-fails at `pre-push`, but only when pushing to `main` — it checks whether files exposed via `.pre-commit-hooks.yaml` changed since the last tag (or no tag exists yet) and fails if so. Silent no-op on feature branches, so it never forces a premature tag on a commit that might not survive a squash-merge. Full details in `docs/adr/0014-vale-prefilter-ships-from-the-plugin.md`. First release tag is intentionally deferred until this merges to `main` — tracked in #87.
Defame1297 added 1 commit 2026-08-09 11:23:03 +00:00
A review of PR #85's last two commits (1164f3a, 4d018af) found the new
release-gate script fails open in four separate ways, and the new drift
check for the duplicated Vale styles only ever detects drift after a
human already hand-edited both copies out of sync.

check-release-needed.sh:
- The `-e` existence filter dropped a RELEASE_PATHS entry from the diff
  pathspec once it was deleted from the tree, so deleting a path exposed
  via .pre-commit-hooks.yaml since the last tag passed the gate clean —
  exactly the breakage the gate exists to catch. git diff reports
  deletions fine without an existence check; the filter is gone.
- `git diff ... 2>/dev/null || true` turned any git failure (a shallow
  clone missing the tag's objects, a corrupted ref) into an empty,
  falsely-clean diff. The diff result is no longer swallowed: a failure
  now hard-fails with the underlying git error visible.
- RELEASE_PATHS was a hand-maintained array duplicating
  .pre-commit-hooks.yaml's entry: paths with only a comment holding them
  in sync, and was already over-broad (it swept in validate.sh /
  validate-provenance.sh, which no hook entry references). It's now
  parsed straight from .pre-commit-hooks.yaml's entry: lines at
  runtime, so it can't drift from the manifest and only tracks what a
  hook actually exposes.
- `git describe --tags --abbrev=0` accepted any tag reachable from HEAD
  as the diff baseline, not just release tags. Added
  `--match 'v[0-9]*.[0-9]*.[0-9]*'` so an incidental checkpoint tag
  can't shift the baseline and mask a real release-relevant change.

check-vale-style-sync.sh still only detects drift between skill-audit's
and agent-audit's duplicated vale-wrap.sh/styles/Kyberforge copies
(both copies must exist independently per the plugin's no-cross-skill-
path packaging rule — a symlink would break at install time). Added
scripts/sync-vale-styles.sh to regenerate skill-audit's copy from
agent-audit's canonical one on demand, and pointed the sync check's
failure message at it, so fixing drift is one command instead of a
hand diff across two files.

Also recorded, rather than silently left unfixed: check-release-needed.sh
only fires on a local `git push` through pre-commit's pre-push hook — a
PR merged via Gitea's merge button, or CI invoking
`pre-commit run --hook-stage pre-push` directly, never sets
PRE_COMMIT_REMOTE_BRANCH and skips the gate entirely. Closing that needs
a server-side CI job this repo doesn't have yet; documented as a known
limitation in ADR-0014 rather than papered over.

Separately, LESSONS.md's "a clean check can mean nothing ran" entry was
marked **Graduated** without ever being promoted per the repo's own
graduation rule (3+ instances → a standing doc, marked
`[graduated → target file]`). Actually promoted it into
core/instructions/testing.md and fixed the marker.

tests/test-check-release-needed.sh gained 4 regression tests, one per
check-release-needed.sh fix above, each verified to fail against the
pre-fix script and pass against the current one.

Verification: bash tests/run-tests.sh (11 scripts + 125 bats, all
passing), pre-commit run --all-files, and
pre-commit run --all-files --hook-stage pre-push all clean.

ADR: 0014
Author
Collaborator

Review of the two follow-up commits (1164f3a, 4d018af) — read-only, then fixed

Ran a full code review against 1164f3a^..4d018af (the portability move + the release-tag hard-fail hook added after round 3 closed). 7 findings, all in the new check-release-needed.sh / check-vale-style-sync.sh machinery — nothing in the portability move itself. Fixed all 7 in acd2f1d.

scripts/check-release-needed.sh — the gate could fail open four ways

  • Deleted paths silently escaped the check. The -e existence filter dropped a RELEASE_PATHS entry from the diff pathspec once it no longer existed at HEAD — so deleting a path .pre-commit-hooks.yaml exposes, between tags, passed the gate clean. git diff reports deletions fine without an existence check; the filter is gone.
  • A git failure read as "nothing changed." git diff ... 2>/dev/null || true turned any git error (shallow clone missing the tag's objects, a bad ref) into an empty, falsely-clean diff. Verified by deleting the tagged commit's tree object directly (describe still resolves the tag name; diff against it then fails) — the gate now hard-fails on that instead of passing. Same "clean means nothing ran" shape as the round-1/round-3 findings on this PR.
  • RELEASE_PATHS was hand-maintained and already over-broad — a parallel array duplicating .pre-commit-hooks.yaml's entry: paths with only a comment keeping them in sync, and it swept in validate.sh/validate-provenance.sh, which no hook entry actually references. Now parsed straight from .pre-commit-hooks.yaml's entry: lines at runtime, so it can't drift from the manifest and only tracks what's actually exposed.
  • No version-tag filter on the baseline. git describe --tags --abbrev=0 accepted any tag reachable from HEAD, so an incidental checkpoint/experiment tag could shift the diff baseline and mask a real release-relevant change. Added --match 'v[0-9]*.[0-9]*.[0-9]*'.
  • Known gap, documented rather than silently left open: this only fires on a local git push through pre-commit's pre-push hook. A PR merged via Gitea's merge button (server-side, no local push — how this repo's PRs actually land) or CI invoking pre-commit run --hook-stage pre-push directly never sets PRE_COMMIT_REMOTE_BRANCH, so the gate silently doesn't run in either path. Closing it needs a server-side CI job this repo doesn't have yet — recorded as a known limitation in ADR-0014's Consequences instead of overstating coverage.

Added 4 regression tests to tests/test-check-release-needed.sh, one per fix above, each checked for non-vacuity (fails against the pre-fix script, passes against the current one).

scripts/check-vale-style-sync.sh — detection without a fix

It only ever detected drift between skill-audit's and agent-audit's duplicated vale-wrap.sh/styles/Kyberforge copies after a human had already hand-edited them out of sync (both copies must exist independently — a plugin's cache-install only copies each skill's own files, so a symlink would break at install time). Added scripts/sync-vale-styles.sh to regenerate skill-audit's copy from agent-audit's canonical one on demand, and pointed the sync check's failure message at it — fixing drift is now one command instead of a hand diff across two files.

Also cleaned up in the same pass

LESSONS.md's "a clean check can mean nothing ran" entry was marked Graduated without ever being promoted per the repo's own graduation rule (3+ instances → a standing doc, marked [graduated → target file]). Actually promoted it into core/instructions/testing.md and fixed the marker.

Verification: bash tests/run-tests.sh (11 scripts + 125 bats, all passing), pre-commit run --all-files, and pre-commit run --all-files --hook-stage pre-push all clean — including the newly-hardened check-release-needed/check-vale-style-sync hooks passing against their own fix.

## Review of the two follow-up commits (1164f3a, 4d018af) — read-only, then fixed Ran a full code review against `1164f3a^..4d018af` (the portability move + the release-tag hard-fail hook added after round 3 closed). 7 findings, all in the new `check-release-needed.sh` / `check-vale-style-sync.sh` machinery — nothing in the portability move itself. Fixed all 7 in `acd2f1d`. ### `scripts/check-release-needed.sh` — the gate could fail open four ways - **Deleted paths silently escaped the check.** The `-e` existence filter dropped a `RELEASE_PATHS` entry from the diff pathspec once it no longer existed at HEAD — so deleting a path `.pre-commit-hooks.yaml` exposes, between tags, passed the gate clean. `git diff` reports deletions fine without an existence check; the filter is gone. - **A git failure read as "nothing changed."** `git diff ... 2>/dev/null || true` turned any git error (shallow clone missing the tag's objects, a bad ref) into an empty, falsely-clean diff. Verified by deleting the tagged commit's tree object directly (`describe` still resolves the tag name; `diff` against it then fails) — the gate now hard-fails on that instead of passing. Same "clean means nothing ran" shape as the round-1/round-3 findings on this PR. - **`RELEASE_PATHS` was hand-maintained and already over-broad** — a parallel array duplicating `.pre-commit-hooks.yaml`'s `entry:` paths with only a comment keeping them in sync, and it swept in `validate.sh`/`validate-provenance.sh`, which no hook entry actually references. Now parsed straight from `.pre-commit-hooks.yaml`'s `entry:` lines at runtime, so it can't drift from the manifest and only tracks what's actually exposed. - **No version-tag filter on the baseline.** `git describe --tags --abbrev=0` accepted any tag reachable from HEAD, so an incidental checkpoint/experiment tag could shift the diff baseline and mask a real release-relevant change. Added `--match 'v[0-9]*.[0-9]*.[0-9]*'`. - **Known gap, documented rather than silently left open:** this only fires on a local `git push` through pre-commit's pre-push hook. A PR merged via Gitea's merge button (server-side, no local push — how this repo's PRs actually land) or CI invoking `pre-commit run --hook-stage pre-push` directly never sets `PRE_COMMIT_REMOTE_BRANCH`, so the gate silently doesn't run in either path. Closing it needs a server-side CI job this repo doesn't have yet — recorded as a known limitation in ADR-0014's Consequences instead of overstating coverage. Added 4 regression tests to `tests/test-check-release-needed.sh`, one per fix above, each checked for non-vacuity (fails against the pre-fix script, passes against the current one). ### `scripts/check-vale-style-sync.sh` — detection without a fix It only ever detected drift between skill-audit's and agent-audit's duplicated `vale-wrap.sh`/`styles/Kyberforge` copies after a human had already hand-edited them out of sync (both copies must exist independently — a plugin's cache-install only copies each skill's own files, so a symlink would break at install time). Added `scripts/sync-vale-styles.sh` to regenerate skill-audit's copy from agent-audit's canonical one on demand, and pointed the sync check's failure message at it — fixing drift is now one command instead of a hand diff across two files. ### Also cleaned up in the same pass `LESSONS.md`'s "a clean check can mean nothing ran" entry was marked **Graduated** without ever being promoted per the repo's own graduation rule (3+ instances → a standing doc, marked `[graduated → target file]`). Actually promoted it into `core/instructions/testing.md` and fixed the marker. **Verification:** `bash tests/run-tests.sh` (11 scripts + 125 bats, all passing), `pre-commit run --all-files`, and `pre-commit run --all-files --hook-stage pre-push` all clean — including the newly-hardened `check-release-needed`/`check-vale-style-sync` hooks passing against their own fix.
Author
Collaborator

Review round 4 — validated

Two-pass review: an adversarial review agent, then an independent validator tasked with falsifying each finding. The validator rejected 2 findings outright, downgraded 2, and raised 1 — the list below is post-validation only.

Verdict: needs work. One Critical, one High. The in-repo path is solid — tests/run-tests.sh 11 passed / 0 failed (144 assertions), check-manifests.sh exit 0, pre-commit run --all-files all 17 hooks pass, all 9 rule files verifiably level: error, and flattening holds across CRLF, apostrophes, backslashes, non-ASCII, blank lines in folded scalars, | literal scalars, spaces in paths, absolute paths and odd cwd. Both defects that matter are at the boundary: the external-consumer contract, and non-Linux hosts.


1. Critical — .pre-commit-hooks.yaml:4,11 — the external-consumer contract is non-functional

Both Vale hook entries pass --config plugins/kyberforge/…/assets/vale/.vale.ini as an argument. pre-commit prefixes only entry[0] with the hook-repo clone path — pre_commit/languages/unsupported_script.py:

cmd = lang_base.hook_cmd(entry, args)
cmd = (prefix.path(cmd[0]), *cmd[1:])

Remaining args are passed verbatim and resolve against the consuming repo's root. Reproduced against a real file:// bare remote (not just a local path), with both a SKILL.md and an agent file present so both hooks actually fire:

Kyberforge Vale prose audit (SKILL.md)...................................Failed
- hook id: kyberforge-vale-audit-skill — exit code: 2
Kyberforge Vale prose audit (agent files)................................Failed
- hook id: kyberforge-vale-audit-agent — exit code: 2

E100 [--config] Runtime error
path '/…/consumer/plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini' does not exist

SKILL.md size ceiling....................................................Passed

So 2 of the 3 hooks ADR-0014 promises are unusable, and the premise of check-release-needed.sh (gate main so consumers can pin a working rev:) does not hold.

Why three rounds missed it: the byte-identical entry string passes in-repo, because repo: local makes prefix == cwd == repo root. The defect is structurally invisible from inside the repo, and nothing in tests/ exercises .pre-commit-hooks.yaml as a hook repo. That is the exact failure mode ADR-0014:79-80 states it was designing against ("the same portability path an external repo would, not … a special root-only case that never gets exercised the way external consumers exercise it"). The relocation did not achieve its stated goal.

Fix: make the wrapper self-locating and drop --config from the manifest. entry[0] is resolved into the clone (proven — the script ran), so default the config in vale-wrap.sh to $(cd "$(dirname "${BASH_SOURCE[0]}")/../assets/vale" && pwd)/.vale.ini when none is supplied. Then add a test that stands up a throwaway consumer repo and runs pre-commit run --all-files against the manifest.

2. High — vale-wrap.sh:81,96 — realpath -m is GNU-only, no fallback

realpath -m (resolve without requiring components to exist) is a GNU coreutils extension; macOS realpath is FreeBSD-derived and has no -m. No fallback, no platform detection (grep -E 'greadlink|uname|Darwin|command -v realpath' → no matches).

-m is load-bearing, not incidental — at line 96 dest does not exist yet (mkdir -p is line 107):

realpath -m /nonexistent/deep/path/x.md  → /nonexistent/deep/path/x.md   exit 0
realpath    /nonexistent/deep/path/x.md  → No such file or directory     exit 1

Under set -euo pipefail that aborts the script. GNU coreutils is documented nowhere in the repo (grep -rn coreutils over all .md/.sh/.yaml → zero hits), while AGENTS.md:25 lists macOS first for these exact hooks (brew install vale (macOS)). A macOS contributor following the repo's own setup docs has every commit touching a SKILL.md or agent file blocked with an opaque illegal option -- m.

Raised from Medium during validation: the repo explicitly claims macOS support for this code path, the failure is total, and there is no documented workaround. BSD behavior is reasoned from the FreeBSD/coreutils divergence, not measured — no Mac available.

Fix: replace both calls with python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' (python3 is already a hard dependency of this script) or a pure-bash normalizer.

3. Medium — vale-wrap.sh:58 — a directory argument silently skips flattening

The classifier [[ "$arg" != -* && -f "$arg" ]] is false for anything that is not a regular file, so a directory lands in vale_args, file_args stays empty, and line 78 execs bare vale — skipping the flattening the script exists to perform. Same file, two invocation forms:

=== explicit file (flattening applies) ===
 3:15  error  Description opens with 'This skill' …   Kyberforge.DescriptionOpener
✖ 1 error, 0 warnings and 0 suggestions in 1 file.      exit=1

=== directory arg ===
✔ 0 errors, 0 warnings and 0 suggestions in 1 file.     exit=0

Lowered from High during validation. Not reachable via pre-commit (run_xargs passes explicit filenames) nor via either audit skill's Step 1 (both prescribe explicit file paths). Reachable only through the lint plugin's vale-run skill / lint-runner agent, whose documented default form is vale <path-or-glob> (vale-run/SKILL.md:32) and whose Gotcha (line 25) tells an agent to reuse the same arguments with a wrapper. Both realistic sweep forms under-report:

unexpanded ** glob → ✔ 0 errors … in stdin.   exit=0
directory form '.' → ✔ 0 errors … in 1 file.  exit=0

Fix: walk a directory argument and flatten each candidate, or refuse it with a clear error. (A nonexistent path is not a defect — it behaves identically to bare vale, which is the wrapper's stated drop-in contract.)

4. Low — plugins/lint/docs/research/docs/vale/cli-reference.md:29 — exit-code model stated backwards

"By default, vale exits non-zero when it finds any alert at or above MinAlertLevel."

Disproved with a level: warning rule under MinAlertLevel = suggestion:

 1:8  warning  warn-level hit: 'bananas'  T.Warn
✖ 0 errors, 1 warning and 0 suggestions in 1 file.      exit=0

Non-zero keys on error-level alerts alone; MinAlertLevel is display-only. This is the precise misconception LESSONS.md's "One signal, two consumers" entry records as costing two review rounds — and this file is the cited provenance source for the skills that now state it correctly. A future author who extends the harness from the research doc rather than the skill ships an invisible non-gating rule.

Correction to the first-pass review: it also flagged vale-run/references/troubleshooting.md:56. That was misattributed — that file is correct and precise at line 68. Only the research sibling docs/research/docs/vale/troubleshooting.md:56 is loosely worded in the same direction.

5. Low — CONTEXT.md — the NOT-RUN rationale is factually wrong

"A skill outside plugins/ (e.g. project-scope .claude/skills/foo/SKILL.md) matches no glob section."

It matches fine:

=== .claude/skills/foo/SKILL.md, skill-audit config ===
 3:14  error  Description opens with 'This skill' …
 3:25  error  Vague capability wording: 'helps with' …
 5:12  error  Generic reference pointer …
✖ 3 errors … in 1 file.   exit=1

[**/SKILL.md] matches any path ending in SKILL.md — as the same paragraph concedes two sentences earlier ("Vale's * crosses /"). Reads like a stale leftover from a path-scoped glob. The same paragraph also cites the local files: regex ^plugins/[^/]+/skills/[^/]+/SKILL\.md$ as the scoping mechanism, where .pre-commit-hooks.yaml actually ships (^|/)SKILL\.md$ to external consumers.

6. Nit — tests/test-vale-wrap.sh:21-24

The whole Vale suite reports green when the vale binary is absent (run-tests.sh:44 increments PASSED on exit 0), so the summary reads 11 passed, 0 failed either way. This PR added the Vale install pointer to AGENTS.md precisely because people hit "command not found". Consider a SKIPPED count in the summary.

7. Info — scripts/check-vale-style-sync.sh:20-22

if [[ ! -d "$SKILL_AUDIT" || ! -d "$AGENT_AUDIT" ]]; then exit 0; fi — the || means one copy missing also passes vacuously, where the intended no-op is both missing. Deliberate and tested for the both-missing case; bounded impact.


Rejected during validation

Recorded so they don't resurface next round:

  • The NOT-RUN 0 files guard is correct. The first pass claimed explicit file paths matching no glob section emit in stdin. rather than 0 files, defeating the guard. False — an existing file matching no section emits ✔ 0 errors … in 0 files., exactly what the guard checks. in stdin. appears only for a nonexistent path, which is identical to bare vale's behavior.
  • No release-gate deadlock. The first pass claimed check-release-needed.sh is unpassable because tagging the merge commit requires the blocked push. Tagging is a purely local operation that precedes the push: tagging local HEAD then running the gate as if pushing to main → exit 0. The gate is also a no-op on this branch (no PRE_COMMIT_REMOTE_BRANCH → exit 0), and the script's own header (lines 12-16) already documents the Gitea merge-button hole.
  • bash 3.2 empty-array expansion (vale-wrap.sh:78) — reachable only with zero arguments, which is a broken invocation on any bash.
  • python3 as an undocumented dependency — pre-existing repo-wide on main (agentsmd-audit, provider-adapter-author, both audit skills' validate.sh), and both audit skills already name "python3 unavailable" as a fallback condition.

Verified as genuinely fixed from rounds 1-3

  • Vale exit-code model — vale-run/SKILL.md:22, references/troubleshooting.md:68, ADR-0013:55-56 all correct now (one stale copy remains — finding 4).
  • Rules shipped at warning — all 9 rule files across both copies are level: error; a deliberately-broken SKILL.md returns exit 1 with all 4 rules firing.
  • --config cwd resolution — works from /, from nested subdirectories via ../../../../, in both --config X and --config=X forms, and with spaces in paths.
  • Multi-line folded descriptions — flattening fires, line numbers preserved. The >-only narrowing is safe: bare Vale genuinely does match | literal scalars, so test 11's wrapped == bare assertion is non-vacuous (baseline confirmed to be a real 3-alert report, not empty).
  • skill-size-check boundary — MAX_LINES=500 with > MAX_LINES; tests assert both the exactly-500 pass and the 501 fail, fixture line counts self-verified.
  • Style drift between the two copies — check-vale-style-sync.sh exits 0; tests cover differing wrapper, differing rule content, and a rule present in only one copy. Both vale-wrap.sh copies are byte-identical (blob 918f32e).

Checked clean

skill-size-check.sh (unterminated-final-line counting, inclusive boundaries, exit propagation) · check-release-needed.sh parsing (RELEASE_PATHS derivation, dirname capturing the sibling styles/ tree, git describe --match excluding non-release tags, fail-closed on shallow clone) · all 5 rules firing on a purpose-built bad fixture with correct line/column/rule-ID · KyberforgeCopilot.ProactivePhrase firing on .agent.md and correctly not on plain agents/*.md, per ADR-0013 · .vale.ini StylesPath relative resolution and section precedence · .pre-commit-config.yaml files: regexes anchored and non-/-crossing, check-hooks-apply and check-useless-excludes pass · wrapper scratch-tree handling (mktemp -d + trap, no leaks across ~20 invocations, ..-escape guard, < /dev/null stdin guard, block-scalar parser across >-/>+/blank lines/indented keys) · executable bits on all new scripts · manifests, claude plugin validate --strict, marketplace parity across both files, version bumps per ADR-0006 · lint-runner agent pair (CC/Copilot differ only in tools:, report-only) · new tests exercise both pass and fail paths with self-verifying boundary fixtures · governance/secrets: no credentials, tokens, connection strings or generated cryptographic material; no copyleft fragments · shellcheck --severity=warning clean.


🤖 Two-agent review (adversarial pass + independent validation) via Claude Code

## Review round 4 — validated Two-pass review: an adversarial review agent, then an independent validator tasked with falsifying each finding. The validator rejected 2 findings outright, downgraded 2, and raised 1 — the list below is post-validation only. **Verdict: needs work.** One Critical, one High. The in-repo path is solid — `tests/run-tests.sh` 11 passed / 0 failed (144 assertions), `check-manifests.sh` exit 0, `pre-commit run --all-files` all 17 hooks pass, all 9 rule files verifiably `level: error`, and flattening holds across CRLF, apostrophes, backslashes, non-ASCII, blank lines in folded scalars, `|` literal scalars, spaces in paths, absolute paths and odd cwd. Both defects that matter are at the boundary: the external-consumer contract, and non-Linux hosts. --- ### 1. Critical — `.pre-commit-hooks.yaml:4,11` — the external-consumer contract is non-functional Both Vale hook entries pass `--config plugins/kyberforge/…/assets/vale/.vale.ini` as an argument. pre-commit prefixes **only `entry[0]`** with the hook-repo clone path — `pre_commit/languages/unsupported_script.py`: ```python cmd = lang_base.hook_cmd(entry, args) cmd = (prefix.path(cmd[0]), *cmd[1:]) ``` Remaining args are passed verbatim and resolve against the **consuming** repo's root. Reproduced against a real `file://` bare remote (not just a local path), with both a `SKILL.md` and an agent file present so both hooks actually fire: ``` Kyberforge Vale prose audit (SKILL.md)...................................Failed - hook id: kyberforge-vale-audit-skill — exit code: 2 Kyberforge Vale prose audit (agent files)................................Failed - hook id: kyberforge-vale-audit-agent — exit code: 2 E100 [--config] Runtime error path '/…/consumer/plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini' does not exist SKILL.md size ceiling....................................................Passed ``` So 2 of the 3 hooks ADR-0014 promises are unusable, and the premise of `check-release-needed.sh` (gate `main` so consumers can pin a working `rev:`) does not hold. **Why three rounds missed it:** the byte-identical entry string *passes* in-repo, because `repo: local` makes prefix == cwd == repo root. The defect is structurally invisible from inside the repo, and nothing in `tests/` exercises `.pre-commit-hooks.yaml` as a hook repo. That is the exact failure mode ADR-0014:79-80 states it was designing against ("the same portability path an external repo would, not … a special root-only case that never gets exercised the way external consumers exercise it"). The relocation did not achieve its stated goal. **Fix:** make the wrapper self-locating and drop `--config` from the manifest. `entry[0]` *is* resolved into the clone (proven — the script ran), so default the config in `vale-wrap.sh` to `$(cd "$(dirname "${BASH_SOURCE[0]}")/../assets/vale" && pwd)/.vale.ini` when none is supplied. Then add a test that stands up a throwaway consumer repo and runs `pre-commit run --all-files` against the manifest. ### 2. High — `vale-wrap.sh:81,96` — `realpath -m` is GNU-only, no fallback `realpath -m` (resolve without requiring components to exist) is a GNU coreutils extension; macOS `realpath` is FreeBSD-derived and has no `-m`. No fallback, no platform detection (`grep -E 'greadlink|uname|Darwin|command -v realpath'` → no matches). `-m` is **load-bearing**, not incidental — at line 96 `dest` does not exist yet (`mkdir -p` is line 107): ``` realpath -m /nonexistent/deep/path/x.md → /nonexistent/deep/path/x.md exit 0 realpath /nonexistent/deep/path/x.md → No such file or directory exit 1 ``` Under `set -euo pipefail` that aborts the script. GNU coreutils is documented nowhere in the repo (`grep -rn coreutils` over all `.md`/`.sh`/`.yaml` → **zero hits**), while `AGENTS.md:25` lists macOS first for these exact hooks (`brew install vale` (macOS)). A macOS contributor following the repo's own setup docs has every commit touching a `SKILL.md` or agent file blocked with an opaque `illegal option -- m`. *Raised from Medium during validation:* the repo explicitly claims macOS support for this code path, the failure is total, and there is no documented workaround. BSD behavior is reasoned from the FreeBSD/coreutils divergence, not measured — no Mac available. **Fix:** replace both calls with `python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))'` (python3 is already a hard dependency of this script) or a pure-bash normalizer. ### 3. Medium — `vale-wrap.sh:58` — a directory argument silently skips flattening The classifier `[[ "$arg" != -* && -f "$arg" ]]` is false for anything that is not a regular file, so a directory lands in `vale_args`, `file_args` stays empty, and line 78 `exec`s bare `vale` — skipping the flattening the script exists to perform. Same file, two invocation forms: ``` === explicit file (flattening applies) === 3:15 error Description opens with 'This skill' … Kyberforge.DescriptionOpener ✖ 1 error, 0 warnings and 0 suggestions in 1 file. exit=1 === directory arg === ✔ 0 errors, 0 warnings and 0 suggestions in 1 file. exit=0 ``` *Lowered from High during validation.* Not reachable via pre-commit (`run_xargs` passes explicit filenames) nor via either audit skill's Step 1 (both prescribe explicit file paths). Reachable only through the `lint` plugin's `vale-run` skill / `lint-runner` agent, whose documented default form is `vale <path-or-glob>` (`vale-run/SKILL.md:32`) and whose Gotcha (line 25) tells an agent to reuse the same arguments with a wrapper. Both realistic sweep forms under-report: ``` unexpanded ** glob → ✔ 0 errors … in stdin. exit=0 directory form '.' → ✔ 0 errors … in 1 file. exit=0 ``` **Fix:** walk a directory argument and flatten each candidate, or refuse it with a clear error. (A *nonexistent* path is not a defect — it behaves identically to bare vale, which is the wrapper's stated drop-in contract.) ### 4. Low — `plugins/lint/docs/research/docs/vale/cli-reference.md:29` — exit-code model stated backwards > "By default, `vale` exits non-zero when it finds any alert at or above `MinAlertLevel`." Disproved with a `level: warning` rule under `MinAlertLevel = suggestion`: ``` 1:8 warning warn-level hit: 'bananas' T.Warn ✖ 0 errors, 1 warning and 0 suggestions in 1 file. exit=0 ``` Non-zero keys on `error`-level alerts alone; `MinAlertLevel` is display-only. This is the precise misconception `LESSONS.md`'s "One signal, two consumers" entry records as costing two review rounds — and this file is the cited provenance source for the skills that now state it correctly. A future author who extends the harness from the research doc rather than the skill ships an invisible non-gating rule. *Correction to the first-pass review:* it also flagged `vale-run/references/troubleshooting.md:56`. That was misattributed — that file is correct and precise at line 68. Only the research sibling `docs/research/docs/vale/troubleshooting.md:56` is loosely worded in the same direction. ### 5. Low — `CONTEXT.md` — the NOT-RUN rationale is factually wrong > "A skill outside `plugins/` (e.g. project-scope `.claude/skills/foo/SKILL.md`) matches no glob section." It matches fine: ``` === .claude/skills/foo/SKILL.md, skill-audit config === 3:14 error Description opens with 'This skill' … 3:25 error Vague capability wording: 'helps with' … 5:12 error Generic reference pointer … ✖ 3 errors … in 1 file. exit=1 ``` `[**/SKILL.md]` matches any path ending in `SKILL.md` — as the same paragraph concedes two sentences earlier ("Vale's `*` crosses `/`"). Reads like a stale leftover from a path-scoped glob. The same paragraph also cites the **local** `files:` regex `^plugins/[^/]+/skills/[^/]+/SKILL\.md$` as the scoping mechanism, where `.pre-commit-hooks.yaml` actually ships `(^|/)SKILL\.md$` to external consumers. ### 6. Nit — `tests/test-vale-wrap.sh:21-24` The whole Vale suite reports green when the `vale` binary is absent (`run-tests.sh:44` increments `PASSED` on exit 0), so the summary reads `11 passed, 0 failed` either way. This PR added the Vale install pointer to `AGENTS.md` precisely because people hit "command not found". Consider a `SKIPPED` count in the summary. ### 7. Info — `scripts/check-vale-style-sync.sh:20-22` `if [[ ! -d "$SKILL_AUDIT" || ! -d "$AGENT_AUDIT" ]]; then exit 0; fi` — the `||` means *one* copy missing also passes vacuously, where the intended no-op is *both* missing. Deliberate and tested for the both-missing case; bounded impact. --- ### Rejected during validation Recorded so they don't resurface next round: - **The NOT-RUN `0 files` guard is correct.** The first pass claimed explicit file paths matching no glob section emit `in stdin.` rather than `0 files`, defeating the guard. False — an *existing* file matching no section emits `✔ 0 errors … in 0 files.`, exactly what the guard checks. `in stdin.` appears only for a nonexistent path, which is identical to bare vale's behavior. - **No release-gate deadlock.** The first pass claimed `check-release-needed.sh` is unpassable because tagging the merge commit requires the blocked push. Tagging is a purely local operation that precedes the push: tagging local HEAD then running the gate as if pushing to `main` → exit 0. The gate is also a no-op on this branch (no `PRE_COMMIT_REMOTE_BRANCH` → exit 0), and the script's own header (lines 12-16) already documents the Gitea merge-button hole. - **bash 3.2 empty-array expansion** (`vale-wrap.sh:78`) — reachable only with zero arguments, which is a broken invocation on any bash. - **`python3` as an undocumented dependency** — pre-existing repo-wide on `main` (`agentsmd-audit`, `provider-adapter-author`, both audit skills' `validate.sh`), and both audit skills already name "python3 unavailable" as a fallback condition. ### Verified as genuinely fixed from rounds 1-3 - Vale exit-code model — `vale-run/SKILL.md:22`, `references/troubleshooting.md:68`, `ADR-0013:55-56` all correct now (one stale copy remains — finding 4). - Rules shipped at `warning` — all 9 rule files across both copies are `level: error`; a deliberately-broken `SKILL.md` returns exit 1 with all 4 rules firing. - `--config` cwd resolution — works from `/`, from nested subdirectories via `../../../../`, in both `--config X` and `--config=X` forms, and with spaces in paths. - Multi-line folded descriptions — flattening fires, line numbers preserved. The `>`-only narrowing is safe: bare Vale genuinely does match `|` literal scalars, so test 11's `wrapped == bare` assertion is non-vacuous (baseline confirmed to be a real 3-alert report, not empty). - `skill-size-check` boundary — `MAX_LINES=500` with `> MAX_LINES`; tests assert both the exactly-500 pass and the 501 fail, fixture line counts self-verified. - Style drift between the two copies — `check-vale-style-sync.sh` exits 0; tests cover differing wrapper, differing rule content, and a rule present in only one copy. Both `vale-wrap.sh` copies are byte-identical (blob `918f32e`). ### Checked clean `skill-size-check.sh` (unterminated-final-line counting, inclusive boundaries, exit propagation) · `check-release-needed.sh` parsing (`RELEASE_PATHS` derivation, `dirname` capturing the sibling `styles/` tree, `git describe --match` excluding non-release tags, fail-closed on shallow clone) · all 5 rules firing on a purpose-built bad fixture with correct line/column/rule-ID · `KyberforgeCopilot.ProactivePhrase` firing on `.agent.md` and correctly *not* on plain `agents/*.md`, per ADR-0013 · `.vale.ini` `StylesPath` relative resolution and section precedence · `.pre-commit-config.yaml` `files:` regexes anchored and non-`/`-crossing, `check-hooks-apply` and `check-useless-excludes` pass · wrapper scratch-tree handling (`mktemp -d` + `trap`, no leaks across ~20 invocations, `..`-escape guard, `< /dev/null` stdin guard, block-scalar parser across `>-`/`>+`/blank lines/indented keys) · executable bits on all new scripts · manifests, `claude plugin validate --strict`, marketplace parity across both files, version bumps per ADR-0006 · `lint-runner` agent pair (CC/Copilot differ only in `tools:`, report-only) · new tests exercise both pass and fail paths with self-verifying boundary fixtures · governance/secrets: no credentials, tokens, connection strings or generated cryptographic material; no copyleft fragments · `shellcheck --severity=warning` clean. --- 🤖 Two-agent review (adversarial pass + independent validation) via [Claude Code](https://claude.com/claude-code)
Defame1297 added 5 commits 2026-08-09 13:09:06 +00:00
pre-commit prefixes only entry[0] with the hook-repo clone path
(cmd = (prefix.path(cmd[0]), *cmd[1:])), so the --config argument in
.pre-commit-hooks.yaml resolved against the *consuming* repo's root
and hard-failed every external run with E100. Two of the three hooks
ADR-0014 promises were unusable.

vale-wrap.sh now self-locates its config from BASH_SOURCE when no
--config is supplied; an explicit --config still wins in all three
argv forms and stays cwd-relative. Both manifests drop the argument
and are kept byte-identical: the local repo: local config resolved
--config correctly only because the consuming repo *was* this repo,
and that divergence is why three review rounds missed the defect.

Also in the wrapper:
- replace GNU-only `realpath -m` with a portable abspath helper; -m is
  load-bearing (dest does not exist yet), so BSD realpath aborted the
  script under set -e on macOS
- walk directory arguments instead of passing them through unflattened,
  which reported a clean 0-error run for files that fail when named
  explicitly
- read/write with errors='surrogateescape' so one non-UTF-8 .md under a
  directory argument cannot abort the hook

New test-vale-hooks-consumer.sh builds the hook repo from the working
tree and points a file:// consumer at it, covering the manifest as a
hook repo for the first time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
The guard used `||`, so exactly one of the two audit skill directories
missing also exited 0, where the intended silent no-op is both absent.
A renamed skill-audit reported green instead of flagging that a
canonical style copy had lost its counterpart.

One-present now exits 1 naming the missing side and the remedy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
The gate derived release-relevant paths from the dirname of each
entry's --config target. Dropping --config from .pre-commit-hooks.yaml
left that loop dead, silently removing both assets/vale/ trees from
coverage — a Vale rule change could land on main without demanding a
release tag, leaving consumers pinned to an old rev: with stale rules.

Coverage now derives from tokens[0] instead: double-dirname for the ..
normalization, guarded on the tree existing and on the bundle root not
resolving to "." so skill-size-check.sh cannot invent a bogus path.

The --config branch is removed rather than kept as dead code. Since
pre-commit rewrites only entry[0], no argument in any entry can ever
name a file this repo ships, so that shape is broken by design.

Known gap: deleting a hook's entire assets/ tree is not flagged, as the
candidate path stops existing. Deletions within a surviving tree are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
cli-reference.md said vale exits non-zero for any alert at or above
MinAlertLevel. The exit code keys on error-level alerts alone;
MinAlertLevel filters display only. LESSONS.md records this exact
misconception as costing two review rounds, and this research doc is
the cited provenance source for the skills that state it correctly.

CONTEXT.md claimed a SKILL.md outside plugins/ matches no glob section.
[**/SKILL.md] matches any path ending in SKILL.md — the sentence is a
stale leftover from the path-scoped globs at cbc33d9, and contradicted
its own paragraph two sentences earlier. The NOT-RUN 0-files guard it
justifies is correct and is unchanged; only the rationale was wrong.
CONTEXT.md also cited the local files: regex as the scoping mechanism,
where the shipped manifest deliberately stays layout-agnostic.

ADR-0014 records the entry[0]-only prefixing constraint as the reason
the self-locating design is required, and that no entry may grow a
repo-internal path argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
kyberforge 1.2.5 -> 1.2.6 for the self-locating vale-wrap.sh.
lint 1.1.3 -> 1.1.4 for the corrected Vale exit-code semantics: a
consumer cached at 1.1.3 holds docs that lead to building a gate which
passes everything.

Both provider manifests bumped in parity per ADR-0006. Marketplace
entries carry no per-plugin version, so neither file changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
Author
Collaborator

Round 4 resolved — acd2f1d → cc5f366

All seven findings from the round-4 report are addressed across five commits. Fixing the Critical finding introduced a regression elsewhere, which is called out below rather than folded in silently.

# Finding Commit
1 Critical — external consumers hard-fail 8c570e9
2 High — realpath -m is GNU-only 8c570e9
3 Medium — directory args skip flattening 8c570e9
4 Low — exit-code model stated backwards e9234f6
5 Low — CONTEXT.md glob claims e9234f6
6 Nit — skips read as passes 8c570e9
7 Info — vacuous style-sync pass 714e8a0
— Regression from #1 — release-gate coverage 348dd9f

1. Critical — the external-consumer contract now works

vale-wrap.sh self-locates its config from ${BASH_SOURCE[0]} when no --config is supplied; --config is dropped from both manifests. An explicit --config still wins in all three argv forms and stays cwd-relative, so both audit skills' Step 1 is unaffected.

Same consumer repo, before and after:

BEFORE  Kyberforge Vale prose audit (SKILL.md).........Failed  exit 2
        Kyberforge Vale prose audit (agent files)......Failed  exit 2
        E100 [--config] Runtime error
        path '/…/consumer/plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini' does not exist

AFTER   Kyberforge Vale prose audit (SKILL.md).........Passed
        Kyberforge Vale prose audit (agent files)......Passed  rc=0

Still gates correctly there — injecting helps with/utilize into a folded description yields exit 1 with two Kyberforge.VagueWording alerts.

.pre-commit-config.yaml also drops the argument, deliberately. Keeping the two entry: lines byte-identical is the actual root-cause fix: the local repo: local hook resolved its --config correctly only because the consuming repo was this repo, so pre-commit run --all-files exercised a path no external consumer takes. That divergence is the whole reason this survived three review rounds. Making them identical means the local run now reproduces what consumers get.

New tests/test-vale-hooks-consumer.sh covers the manifest as a hook repo for the first time. It builds the hook repo from the working tree and commits it, then points a throwaway consumer at file://… — so it tests uncommitted changes, which a bare clone of HEAD could not.

2. High — realpath -m replaced

-m is load-bearing (dest does not exist yet; mkdir -p comes later), so a BSD realpath aborts under set -e. tmpdir now uses cd "$(mktemp -d)" && pwd -P; dest uses an abspath() helper backed by python3 -c 'os.path.abspath', already a hard dependency. The case "$dest" in "$tmpdir"/*) escape guard still refuses a 40-deep ../ climb with exit 2.

The regression test shadows realpath with a stub that rejects -m and mimics BSD's illegal option -- m, so macOS is simulated rather than assumed.

3. Medium — directory arguments are walked

Classifier widened to ( -f "$arg" || -d "$arg" ) — deliberately not -e, so fifos and devices keep the old pass-through. A directory is mirrored whole into the scratch tree (NUL-delimited find/read, so spaces and newlines survive; .git pruned at any depth), then every *.md in the copy is flattened. Mirroring the whole tree rather than a filtered list is intentional: vale applies its own format filtering, so any file dropped here would be silently unlinted — the same defect class as the finding itself.

A non-existent path still falls through untouched, matching bare vale, per the wrapper's drop-in contract.

One hardening this made necessary: the flattener now uses encoding='utf-8', errors='surrogateescape'. Without it, a single non-UTF-8 .md anywhere under a directory argument would abort the hook with a Python traceback — a failure mode the walk itself would have introduced.

4–5. Docs corrections

cli-reference.md:29 now states that only error-level alerts make Vale exit non-zero and that MinAlertLevel filters display in both directions; the --no-exit and --minAlertLevel rows were corrected too, as was the same misconception in the research troubleshooting.md. The rest of docs/research/docs/vale/ was swept — configuration.md:34 and examples.md:23 are accurate and untouched.

CONTEXT.md's false claim is confirmed as a stale leftover: at cbc33d9 the root .vale.ini genuinely did use path-scoped globs ([plugins/*/skills/*/SKILL.md]), and 149d564 changed them to [**/SKILL.md] without updating the prose. The NOT-RUN 0 files guard is unchanged — it is correct, and only the rationale explaining when Vale reports 0 files was wrong. The paragraph now also distinguishes the local files: regex from the layout-agnostic one shipped to consumers.

LESSONS.md:143 was checked and deliberately left alone: its near-identical wording is accurate history of the cbc33d9 design, not a live claim.

ADR-0014 was corrected in place rather than appended to. It was introduced by 1164f3a on this same unmerged branch with no tags cut, so there is no shipped decision history to preserve; an amendment note would record a draft state no reader ever saw. It now records the entry[0]-only prefixing constraint as the reason the self-locating design is required, and forbids either manifest's entry: from growing a repo-internal path argument.

6–7. Test harness and sync check

run-tests.sh treats exit 77 as a distinct SKIPPED outcome, surfaced in the summary and listed by name — a skipped suite can no longer read as a pass. Verified nothing parses the old output format first.

check-vale-style-sync.sh's || became &&, so exactly one copy missing now exits 1 naming the missing side; the both-missing no-op and its test are preserved.


Regression introduced by fix 1, and closed

Dropping --config from the manifest left check-release-needed.sh deriving release-relevant paths from a token that no longer exists, silently removing both assets/vale/ trees from coverage:

BEFORE  manifest, both vale-wrap.sh, skill-size-check.sh,
        + plugins/kyberforge/skills/{skill,agent}-audit/assets/vale
AFTER   manifest, both vale-wrap.sh, skill-size-check.sh

A Vale rule change could then land on main without demanding a release tag, leaving consumers pinned to an older rev: with stale rules — precisely the drift the gate exists to prevent. Coverage now derives from tokens[0] (double-dirname for the .. normalization, no realpath -m reintroduced), guarded on the tree existing and on the bundle root not resolving to . so skill-size-check.sh cannot invent a bogus path. Demonstrated on a scratch repo with a styles-only commit after v1.0.0: exit=0 before, exit=1 with the offending file named after.

The --config parsing branch was removed rather than kept as harmless dead code. It is not merely unused but structurally unusable: since pre-commit rewrites only entry[0], no argument in any entry can ever reference a file this repo ships. The manifest's entry: contract is now "bare script path only," recorded in ADR-0014.

Accepted gap: deleting a hook's entire assets/ tree is not flagged, because the candidate path stops existing and never enters the pathspec. Deletions within a surviving tree are flagged and tested. Closing the whole-tree case needs the last-tag tree consulted rather than the worktree.

Non-vacuity

Every new test was run against the old code and shown to fail:

test-vale-wrap.sh cases 12-15  → old: 15 passed, 4 failed   new: 19 passed, 0 failed
test-check-vale-style-sync #6  → old:  5 passed, 2 failed   new:  7 passed, 0 failed
test-check-release-needed 6,10 → old:  9 passed, 2 failed   new: 11 passed, 0 failed
test-vale-hooks-consumer.sh    → old:  0 passed, 2 failed   new:  2 passed, 0 failed
run-tests.sh skip fixture      ��� old: "1 passed, 2 failed"  new: "1 passed, 1 skipped, 1 failed"

Case 11 of the release-gate suite is vacuous against the old script but caught a real bug in the first iteration of the new one — the missing bundle_root != "." guard, which would have made any future top-level assets/ falsely demand a release tag.

Gates

tests/run-tests.sh              12 passed, 0 skipped, 0 failed  (vale-wrap 19/19)
pre-commit run --all-files      all 17 hooks Passed
scripts/check-manifests.sh      rc=0
scripts/check-vale-style-sync.sh rc=0
claude plugin validate --strict  passes, 6 plugins + marketplace
shellcheck --severity=warning    clean
vale-wrap.sh copies              byte-identical (9e22cb8)
pre-push (full suite + manifest + release + validate)  all Passed

Versions: kyberforge 1.2.5 → 1.2.6 (shipped vale-wrap.sh changed), lint 1.1.3 → 1.1.4 (a consumer cached at 1.1.3 holds docs that lead to building a gate which passes everything). Marketplace entries carry no per-plugin version, so neither manifest changed — matching the precedent in aff5b6c.


🤖 Fixes applied by parallel subagents, verified independently, via Claude Code

## Round 4 resolved — `acd2f1d` → `cc5f366` All seven findings from [the round-4 report](#issuecomment-1344) are addressed across five commits. Fixing the Critical finding introduced a regression elsewhere, which is called out below rather than folded in silently. | # | Finding | Commit | |---|---|---| | 1 | Critical — external consumers hard-fail | `8c570e9` | | 2 | High — `realpath -m` is GNU-only | `8c570e9` | | 3 | Medium — directory args skip flattening | `8c570e9` | | 4 | Low — exit-code model stated backwards | `e9234f6` | | 5 | Low — `CONTEXT.md` glob claims | `e9234f6` | | 6 | Nit — skips read as passes | `8c570e9` | | 7 | Info — vacuous style-sync pass | `714e8a0` | | — | **Regression from #1** — release-gate coverage | `348dd9f` | --- ### 1. Critical — the external-consumer contract now works `vale-wrap.sh` self-locates its config from `${BASH_SOURCE[0]}` when no `--config` is supplied; `--config` is dropped from both manifests. An explicit `--config` still wins in all three argv forms and stays cwd-relative, so both audit skills' Step 1 is unaffected. Same consumer repo, before and after: ``` BEFORE Kyberforge Vale prose audit (SKILL.md).........Failed exit 2 Kyberforge Vale prose audit (agent files)......Failed exit 2 E100 [--config] Runtime error path '/…/consumer/plugins/kyberforge/skills/skill-audit/assets/vale/.vale.ini' does not exist AFTER Kyberforge Vale prose audit (SKILL.md).........Passed Kyberforge Vale prose audit (agent files)......Passed rc=0 ``` Still gates correctly there — injecting `helps with`/`utilize` into a folded description yields exit 1 with two `Kyberforge.VagueWording` alerts. **`.pre-commit-config.yaml` also drops the argument, deliberately.** Keeping the two `entry:` lines byte-identical is the actual root-cause fix: the local `repo: local` hook resolved its `--config` correctly only because the consuming repo *was* this repo, so `pre-commit run --all-files` exercised a path no external consumer takes. That divergence is the whole reason this survived three review rounds. Making them identical means the local run now reproduces what consumers get. New `tests/test-vale-hooks-consumer.sh` covers the manifest as a hook repo for the first time. It builds the hook repo from the **working tree** and commits it, then points a throwaway consumer at `file://…` — so it tests uncommitted changes, which a bare clone of HEAD could not. ### 2. High — `realpath -m` replaced `-m` is load-bearing (`dest` does not exist yet; `mkdir -p` comes later), so a BSD `realpath` aborts under `set -e`. `tmpdir` now uses `cd "$(mktemp -d)" && pwd -P`; `dest` uses an `abspath()` helper backed by `python3 -c 'os.path.abspath'`, already a hard dependency. The `case "$dest" in "$tmpdir"/*)` escape guard still refuses a 40-deep `../` climb with exit 2. The regression test shadows `realpath` with a stub that rejects `-m` and mimics BSD's `illegal option -- m`, so macOS is simulated rather than assumed. ### 3. Medium — directory arguments are walked Classifier widened to `( -f "$arg" || -d "$arg" )` — deliberately not `-e`, so fifos and devices keep the old pass-through. A directory is mirrored whole into the scratch tree (NUL-delimited `find`/`read`, so spaces and newlines survive; `.git` pruned at any depth), then every `*.md` in the copy is flattened. Mirroring the whole tree rather than a filtered list is intentional: vale applies its own format filtering, so any file dropped here would be silently unlinted — the same defect class as the finding itself. A non-existent path still falls through untouched, matching bare vale, per the wrapper's drop-in contract. One hardening this made necessary: the flattener now uses `encoding='utf-8', errors='surrogateescape'`. Without it, a single non-UTF-8 `.md` anywhere under a directory argument would abort the hook with a Python traceback — a failure mode the walk itself would have introduced. ### 4–5. Docs corrections `cli-reference.md:29` now states that only `error`-level alerts make Vale exit non-zero and that `MinAlertLevel` filters display in both directions; the `--no-exit` and `--minAlertLevel` rows were corrected too, as was the same misconception in the research `troubleshooting.md`. The rest of `docs/research/docs/vale/` was swept — `configuration.md:34` and `examples.md:23` are accurate and untouched. `CONTEXT.md`'s false claim is confirmed as a stale leftover: at `cbc33d9` the root `.vale.ini` genuinely did use path-scoped globs (`[plugins/*/skills/*/SKILL.md]`), and `149d564` changed them to `[**/SKILL.md]` without updating the prose. **The NOT-RUN `0 files` guard is unchanged** — it is correct, and only the rationale explaining when Vale reports 0 files was wrong. The paragraph now also distinguishes the local `files:` regex from the layout-agnostic one shipped to consumers. `LESSONS.md:143` was checked and deliberately left alone: its near-identical wording is accurate *history* of the `cbc33d9` design, not a live claim. ADR-0014 was corrected in place rather than appended to. It was introduced by `1164f3a` on this same unmerged branch with no tags cut, so there is no shipped decision history to preserve; an amendment note would record a draft state no reader ever saw. It now records the `entry[0]`-only prefixing constraint as the reason the self-locating design is required, and forbids either manifest's `entry:` from growing a repo-internal path argument. ### 6–7. Test harness and sync check `run-tests.sh` treats exit 77 as a distinct SKIPPED outcome, surfaced in the summary and listed by name — a skipped suite can no longer read as a pass. Verified nothing parses the old output format first. `check-vale-style-sync.sh`'s `||` became `&&`, so exactly one copy missing now exits 1 naming the missing side; the both-missing no-op and its test are preserved. --- ### Regression introduced by fix 1, and closed Dropping `--config` from the manifest left `check-release-needed.sh` deriving release-relevant paths from a token that no longer exists, silently removing both `assets/vale/` trees from coverage: ``` BEFORE manifest, both vale-wrap.sh, skill-size-check.sh, + plugins/kyberforge/skills/{skill,agent}-audit/assets/vale AFTER manifest, both vale-wrap.sh, skill-size-check.sh ``` A Vale **rule** change could then land on `main` without demanding a release tag, leaving consumers pinned to an older `rev:` with stale rules — precisely the drift the gate exists to prevent. Coverage now derives from `tokens[0]` (double-`dirname` for the `..` normalization, no `realpath -m` reintroduced), guarded on the tree existing *and* on the bundle root not resolving to `.` so `skill-size-check.sh` cannot invent a bogus path. Demonstrated on a scratch repo with a styles-only commit after `v1.0.0`: `exit=0` before, `exit=1` with the offending file named after. The `--config` parsing branch was removed rather than kept as harmless dead code. It is not merely unused but structurally unusable: since pre-commit rewrites only `entry[0]`, no argument in any entry can ever reference a file this repo ships. The manifest's `entry:` contract is now "bare script path only," recorded in ADR-0014. **Accepted gap:** deleting a hook's *entire* `assets/` tree is not flagged, because the candidate path stops existing and never enters the pathspec. Deletions *within* a surviving tree are flagged and tested. Closing the whole-tree case needs the last-tag tree consulted rather than the worktree. ### Non-vacuity Every new test was run against the old code and shown to fail: ``` test-vale-wrap.sh cases 12-15 → old: 15 passed, 4 failed new: 19 passed, 0 failed test-check-vale-style-sync #6 → old: 5 passed, 2 failed new: 7 passed, 0 failed test-check-release-needed 6,10 → old: 9 passed, 2 failed new: 11 passed, 0 failed test-vale-hooks-consumer.sh → old: 0 passed, 2 failed new: 2 passed, 0 failed run-tests.sh skip fixture ��� old: "1 passed, 2 failed" new: "1 passed, 1 skipped, 1 failed" ``` Case 11 of the release-gate suite is vacuous against the old script but caught a real bug in the *first* iteration of the new one — the missing `bundle_root != "."` guard, which would have made any future top-level `assets/` falsely demand a release tag. ### Gates ``` tests/run-tests.sh 12 passed, 0 skipped, 0 failed (vale-wrap 19/19) pre-commit run --all-files all 17 hooks Passed scripts/check-manifests.sh rc=0 scripts/check-vale-style-sync.sh rc=0 claude plugin validate --strict passes, 6 plugins + marketplace shellcheck --severity=warning clean vale-wrap.sh copies byte-identical (9e22cb8) pre-push (full suite + manifest + release + validate) all Passed ``` Versions: kyberforge `1.2.5` → `1.2.6` (shipped `vale-wrap.sh` changed), lint `1.1.3` → `1.1.4` (a consumer cached at 1.1.3 holds docs that lead to building a gate which passes everything). Marketplace entries carry no per-plugin version, so neither manifest changed — matching the precedent in `aff5b6c`. --- 🤖 Fixes applied by parallel subagents, verified independently, via [Claude Code](https://claude.com/claude-code)
Defame1297 added 3 commits 2026-08-09 13:34:44 +00:00
Coverage was derived from the worktree alone, so the -d guard on a
hook's bundled assets/ tree meant deleting the whole tree removed it
from the pathspec instead of flagging it — the gate stayed silent
about a change that breaks every consumer at the next rev:.

The path set is now derived twice, from the worktree manifest and from
the manifest at $LAST_TAG, then unioned. A path the tag exposed but
HEAD no longer does is a removal pinned consumers must be told about;
a path only HEAD exposes is new contract surface. Both need flagging.

Fails closed on an unreadable tagged tree (shallow clone), and treats
a readable root tree with no manifest as "added since the tag".

tokens[0] needed no exit-code fix — it carries no existence guard, so
both deletion cases already exited non-zero. What was wrong was the
reporting: a fully retired hook could no longer be named in the
failure message. The tagged manifest fixes that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
Under set -u, "${arr[@]}" on an empty array aborts on bash before 4.4,
which is what macOS ships as /bin/bash. Three expansion sites now use
${arr[@]+"${arr[@]}"} consistently.

The hazard is not currently reachable: verified on a bash 3.2.57 built
from source that all seven invocation shapes succeed against the
previous code, including zero args, flags-only and an empty directory.
vale_args is provably non-empty at every site because the default
--config branch always appends first. The guard is kept because that
invariant is non-local and untested, so an edit to the default-config
branch would reintroduce a macOS-only crash silently.

Test fidelity is deliberately mixed. Case 16 is static and is the only
one that fails against the previous code, since no bash 5 host can
reproduce the abort at runtime. Case 17 runs the emptiest invocations
under the oldest bash it can find and names that shell in its output
so it cannot overclaim. Case 18 guards against the tempting wrong fix
of dropping the quotes, which also silences the abort but word-splits
a path containing a space.

No other bash 4.x construct is present; swept for mapfile, declare -A,
case modification, negative indices, globstar, wait -n and namerefs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
A config's local mode can prove nothing about the mode that ships:
repo: local collapses the clone prefix, cwd and repo root into one
directory, so a byte-identical entry: string worked locally for a
reason that exists only locally, through three review rounds.

Deleting a token from a shared artifact breaks whatever parses it,
silently: dropping --config killed the loop that gave the bundled
Vale styles release coverage, shrinking a derived path list with no
error and no failing test.

Kept separate from the adjacent "clean linter result" and "one signal,
two consumers" entries, which describe different failure modes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
Author
Collaborator

Accepted gaps closed — cc5f366 → afc2b7f

The two gaps logged as accepted in the round-4 resolution are now fixed, plus the round-4 lessons are recorded. One of these did not turn out the way the original finding described it, so that is corrected below rather than quietly shipped.

Commit Change
14c2c91 Release gate flags paths retired since the last tag
16c038b Empty-array expansions guarded for bash 3.2 (+ kyberforge 1.2.7)
afc2b7f Two lessons recorded

1. Release gate — whole-tree deletions now flagged

Coverage was derived from the worktree alone, so the -d guard on a hook's bundled assets/ tree meant deleting the entire tree removed it from the pathspec rather than flagging it. The gate stayed silent about a change that breaks every external consumer at the next rev:.

The path set is now derived twice — from the worktree manifest and from the manifest at $LAST_TAG — and unioned. Union rather than intersection, deliberately: a path the tag exposed but HEAD no longer does is a removal that pinned consumers must be told about, while a path only HEAD exposes is new contract surface they cannot reach without a new tag. Both directions need flagging, and the union cannot over-fire on its own, since any manifest edit that makes the two disagree already changes .pre-commit-hooks.yaml, which is itself in the path set.

A single collect_release_paths <scope> helper serves both derivations — worktree probes with -d, a rev probes with git cat-file -e "$rev:$bundle_root/assets" — so there is one parsing path, not two that can drift. Both existing guards still apply on both sides: scripts/skill-size-check.sh contributes no assets path, and a future top-level assets/ still cannot falsely demand a release.

Fail-closed behavior is disambiguated: an unreadable tagged tree (shallow clone) hard-fails with the existing git fetch --unshallow pointer, while a readable root tree with no manifest is treated as "manifest added since the tag."

tokens[0] was investigated as the suspected twin of this bug, and is not one. It is added unconditionally and never carried an existence guard, so both deletion cases — script deleted with the entry surviving, and entry plus script removed together — already exited non-zero. What was wrong was the reporting: a fully retired hook could no longer be named in the failure message, understating the breakage. The tagged manifest fixes that, and a test now asserts it.

tests/test-check-release-needed.sh: 11 → 14 cases.

old script → 12 passed, 2 failed
new script → 14 passed, 0 failed

Case 13 passes against both old and new code and is labelled in-file as a characterization test, not a fix — it pins the fact that tokens[0] carries no existence guard, so a future refactor of the union cannot silently add one and reopen the hole assets/ had.

2. bash 3.2 — the original finding was wrong about reachability

The round-4 report filed this as an unreachable nit. That was right to be skeptical, but the reasoning behind it was incomplete, and the honest result is worth recording.

The hazard was verified unreachable on a real bash 3.2.57 built from source, not argued from documentation. All seven invocation shapes succeed against cc5f366: zero args, flags-only, unknown-flag-only, file argument, directory argument, empty directory, and a directory containing no .md files. The two shapes I specifically expected to be reachable — flags-only, and a directory with no matching files — do not trip it.

The reason is a non-local invariant: vale_args is provably non-empty at every expansion site, because either config_given is false and the default --config is appended, or config_given was set by one of three branches that each append first.

Three sites are guarded anyway with a single consistent idiom (${arr[@]+"${arr[@]}"}), verified byte-faithful on both 3.2 and 5.2 for empty arrays, embedded spaces, empty-string elements, glob characters, tabs, and a hostile IFS=:. The reasoning for keeping a fix with no live failure behind it: the invariant that makes the current code safe is non-local and untested, so an edit to the default-config branch would reintroduce a macOS-only crash with nothing to catch it.

bash 3.2 is genuinely achievable for this file — no blocker. Confirmed on the 3.2 binary that ${#arr[@]} on an empty array, ${arg#"$cwd"/} nested-quote pattern removal, read -r -d '', process substitution, += append, and BASH_SOURCE all work. A full bash-4.x sweep found nothing: no mapfile/readarray, declare -A, ${var,,}/${var^^}, negative indices, globstar, wait -n, namerefs, coproc, printf -v, ${!...}, or PIPESTATUS.

Test fidelity, stated plainly rather than rounded up. tests/test-vale-wrap.sh: 19 → 22 cases.

  • Case 16 is static and is the only one that fails against cc5f366 (it names all three sites). Static by design: no bash 5 host can reproduce the abort at runtime, so absence of the construct is the only assertable property.
  • Case 17 runs the emptiest invocations under the oldest bash it can find and names that shell in its pass message, so it can never overclaim. On CI it reports bash 5.2. It cannot fail against cc5f366 even on real 3.2 — the honest consequence of the hazard being unreachable. It proves the guarded form does not break anything, not that it fixes a live crash.
  • Case 18 guards against the tempting wrong fix: dropping the quotes also silences the abort, but word-splits a path containing a space.

Real bash 3.2 coverage requires pointing VALE_WRAP_TEST_BASH at a 3.2 binary. macOS coverage is not claimed — a Linux-built 3.2.57 is the right shell version, not the right platform.

3. Lessons recorded

Two entries in LESSONS.md, in the format established by 864e7c6:

  • Exercising a config's "local" mode proves nothing about the mode that ships. repo: local collapses the clone prefix, cwd, and repo root into one directory, so a byte-identical entry: string worked locally for a reason that exists only locally. The local run was not weaker evidence of the same thing — it was evidence of a different thing, and the two were indistinguishable by reading either file.
  • Deleting a token from a shared artifact breaks whatever parses it, silently. The smell: a loop that builds a list, where a short list is indistinguishable from a correct one.

Checked against the adjacent existing entries and kept separate with reasons: "A clean linter result can mean 'nothing was checked'" covers a check that never ran, whereas here the local hook ran and checked a different resolution path; "One signal, two consumers" covers a live signal diverging, not a token's removal killing an undocumented parser. Worth noting the first of those already carries a [graduated → core/instructions/testing.md] marker and took a 4th instance on 2026-08-09 — it and the new local-mode entry are one instance away from being the same cluster.

Gates

tests/run-tests.sh              12 passed, 0 skipped, 0 failed
  vale-wrap                     22 passed, 0 failed
  check-release-needed          14 passed, 0 failed
scripts/check-manifests.sh      rc=0
scripts/check-vale-style-sync.sh rc=0
shellcheck --severity=warning    clean
vale-wrap.sh copies              byte-identical (f3d891b)
pre-push (suite + manifests + release + validate)  all Passed

kyberforge 1.2.6 → 1.2.7 for the shipped wrapper change; marketplace.json untouched, as entries carry no per-plugin version.

Remaining, unchanged

Still open by design, all pre-existing: a hook renamed between tag and HEAD is over-covered rather than tracked as one rename (harmless — the manifest edit fires regardless); the tag-time assets probe is tree-existence, not recursive, so a treeless partial clone could false-negative; the assets bundle-directory name is a hardcoded convention; and the gate only fires on a local git push, so a server-side merge still bypasses it.

That last one matters for landing this PR. git tag -l is empty, and merging through Gitea's button does not run the pre-push hook — so the gate will not fire, the merge will succeed, and .pre-commit-hooks.yaml will expose hooks to external consumers with no rev: anyone can pin. Cutting v1.0.0 on main after the merge needs to be a deliberate step; nothing will prompt for it.


🤖 Fixes applied by parallel subagents, verified independently, via Claude Code

## Accepted gaps closed — `cc5f366` → `afc2b7f` The two gaps logged as accepted in [the round-4 resolution](#issuecomment-1346) are now fixed, plus the round-4 lessons are recorded. One of these did not turn out the way the original finding described it, so that is corrected below rather than quietly shipped. | Commit | Change | |---|---| | `14c2c91` | Release gate flags paths retired since the last tag | | `16c038b` | Empty-array expansions guarded for bash 3.2 (+ kyberforge 1.2.7) | | `afc2b7f` | Two lessons recorded | --- ### 1. Release gate — whole-tree deletions now flagged Coverage was derived from the worktree alone, so the `-d` guard on a hook's bundled `assets/` tree meant deleting the **entire** tree removed it from the pathspec rather than flagging it. The gate stayed silent about a change that breaks every external consumer at the next `rev:`. The path set is now derived twice — from the worktree manifest and from the manifest at `$LAST_TAG` — and unioned. Union rather than intersection, deliberately: a path the tag exposed but HEAD no longer does is a **removal** that pinned consumers must be told about, while a path only HEAD exposes is **new contract surface** they cannot reach without a new tag. Both directions need flagging, and the union cannot over-fire on its own, since any manifest edit that makes the two disagree already changes `.pre-commit-hooks.yaml`, which is itself in the path set. A single `collect_release_paths <scope>` helper serves both derivations — `worktree` probes with `-d`, a rev probes with `git cat-file -e "$rev:$bundle_root/assets"` — so there is one parsing path, not two that can drift. Both existing guards still apply on both sides: `scripts/skill-size-check.sh` contributes no `assets` path, and a future top-level `assets/` still cannot falsely demand a release. Fail-closed behavior is disambiguated: an unreadable tagged tree (shallow clone) hard-fails with the existing `git fetch --unshallow` pointer, while a readable root tree with no manifest is treated as "manifest added since the tag." **`tokens[0]` was investigated as the suspected twin of this bug, and is not one.** It is added unconditionally and never carried an existence guard, so both deletion cases — script deleted with the entry surviving, and entry plus script removed together — already exited non-zero. What *was* wrong was the **reporting**: a fully retired hook could no longer be named in the failure message, understating the breakage. The tagged manifest fixes that, and a test now asserts it. `tests/test-check-release-needed.sh`: 11 → 14 cases. ``` old script → 12 passed, 2 failed new script → 14 passed, 0 failed ``` Case 13 passes against both old and new code and is **labelled in-file as a characterization test**, not a fix — it pins the fact that `tokens[0]` carries no existence guard, so a future refactor of the union cannot silently add one and reopen the hole `assets/` had. ### 2. bash 3.2 — the original finding was wrong about reachability The round-4 report filed this as an unreachable nit. That was right to be skeptical, but the reasoning behind it was incomplete, and the honest result is worth recording. **The hazard was verified unreachable on a real bash 3.2.57 built from source**, not argued from documentation. All seven invocation shapes succeed against `cc5f366`: zero args, flags-only, unknown-flag-only, file argument, directory argument, empty directory, and a directory containing no `.md` files. The two shapes I specifically expected to be reachable — flags-only, and a directory with no matching files — do **not** trip it. The reason is a non-local invariant: `vale_args` is provably non-empty at every expansion site, because either `config_given` is false and the default `--config` is appended, or `config_given` was set by one of three branches that each append first. Three sites are guarded anyway with a single consistent idiom (`${arr[@]+"${arr[@]}"}`), verified byte-faithful on both 3.2 and 5.2 for empty arrays, embedded spaces, empty-string elements, glob characters, tabs, and a hostile `IFS=:`. The reasoning for keeping a fix with no live failure behind it: the invariant that makes the current code safe is non-local and untested, so an edit to the default-config branch would reintroduce a macOS-only crash with nothing to catch it. **bash 3.2 is genuinely achievable for this file — no blocker.** Confirmed on the 3.2 binary that `${#arr[@]}` on an empty array, `${arg#"$cwd"/}` nested-quote pattern removal, `read -r -d ''`, process substitution, `+=` append, and `BASH_SOURCE` all work. A full bash-4.x sweep found nothing: no `mapfile`/`readarray`, `declare -A`, `${var,,}`/`${var^^}`, negative indices, globstar, `wait -n`, namerefs, `coproc`, `printf -v`, `${!...}`, or `PIPESTATUS`. **Test fidelity, stated plainly rather than rounded up.** `tests/test-vale-wrap.sh`: 19 → 22 cases. - Case 16 is **static** and is the only one that fails against `cc5f366` (it names all three sites). Static by design: no bash 5 host can reproduce the abort at runtime, so absence of the construct is the only assertable property. - Case 17 runs the emptiest invocations under the oldest bash it can find and **names that shell in its pass message**, so it can never overclaim. On CI it reports `bash 5.2`. It cannot fail against `cc5f366` even on real 3.2 — the honest consequence of the hazard being unreachable. It proves the guarded form does not break anything, not that it fixes a live crash. - Case 18 guards against the tempting *wrong* fix: dropping the quotes also silences the abort, but word-splits a path containing a space. Real bash 3.2 coverage requires pointing `VALE_WRAP_TEST_BASH` at a 3.2 binary. **macOS coverage is not claimed** — a Linux-built 3.2.57 is the right shell version, not the right platform. ### 3. Lessons recorded Two entries in `LESSONS.md`, in the format established by `864e7c6`: - **Exercising a config's "local" mode proves nothing about the mode that ships.** `repo: local` collapses the clone prefix, cwd, and repo root into one directory, so a byte-identical `entry:` string worked locally for a reason that exists only locally. The local run was not weaker evidence of the same thing — it was evidence of a different thing, and the two were indistinguishable by reading either file. - **Deleting a token from a shared artifact breaks whatever parses it, silently.** The smell: a loop that builds a list, where a short list is indistinguishable from a correct one. Checked against the adjacent existing entries and kept separate with reasons: *"A clean linter result can mean 'nothing was checked'"* covers a check that never ran, whereas here the local hook ran and checked a different resolution path; *"One signal, two consumers"* covers a live signal diverging, not a token's removal killing an undocumented parser. Worth noting the first of those already carries a `[graduated → core/instructions/testing.md]` marker and took a 4th instance on 2026-08-09 — it and the new local-mode entry are one instance away from being the same cluster. ### Gates ``` tests/run-tests.sh 12 passed, 0 skipped, 0 failed vale-wrap 22 passed, 0 failed check-release-needed 14 passed, 0 failed scripts/check-manifests.sh rc=0 scripts/check-vale-style-sync.sh rc=0 shellcheck --severity=warning clean vale-wrap.sh copies byte-identical (f3d891b) pre-push (suite + manifests + release + validate) all Passed ``` kyberforge `1.2.6` → `1.2.7` for the shipped wrapper change; `marketplace.json` untouched, as entries carry no per-plugin version. ### Remaining, unchanged Still open by design, all pre-existing: a hook renamed between tag and HEAD is over-covered rather than tracked as one rename (harmless — the manifest edit fires regardless); the tag-time `assets` probe is tree-existence, not recursive, so a treeless partial clone could false-negative; the `assets` bundle-directory name is a hardcoded convention; and the gate only fires on a local `git push`, so a server-side merge still bypasses it. **That last one matters for landing this PR.** `git tag -l` is empty, and merging through Gitea's button does not run the pre-push hook — so the gate will not fire, the merge will succeed, and `.pre-commit-hooks.yaml` will expose hooks to external consumers with no `rev:` anyone can pin. Cutting `v1.0.0` on `main` after the merge needs to be a deliberate step; nothing will prompt for it. --- 🤖 Fixes applied by parallel subagents, verified independently, via [Claude Code](https://claude.com/claude-code)
Defame1297 added 3 commits 2026-08-09 15:46:43 +00:00
Vale locates a frontmatter description by matching the parsed YAML value back
against the source text, so any scalar whose value is not spelled out verbatim
loses the `text.frontmatter.description` scope entirely. The wrapper only
flattened `>` folded scalars, so plain, double-quoted and single-quoted
multi-line descriptions silently reported zero alerts and exit 0 — a clean pass
indistinguishable from a real one, in a prefilter whose callers are instructed
not to re-derive its verdict by judgment.

Implementation notes:

- Classify the scalar kind after `^description:[ \t]*` and reuse one shared
  continuation-line generator for every form; `|` literal blocks keep their
  line breaks, stay verbatim-matchable, and are still left untouched.
- Emit the flattened value in whichever scalar form needs no escape at all
  (plain, then single-quoted, then double-quoted), because any escape breaks
  the verbatim match. The old blanket `'` -> U+2019 substitution silently made
  apostrophe-bearing rule tokens unmatchable across 63% of the corpus; it now
  survives only for the one combination no YAML scalar can carry verbatim.
- Terminate continuations at a line flush with the key, not only on a shallower
  indent — a `description:` followed by a flush-left line previously swallowed
  the rest of the frontmatter.
- Route vale's value-taking flags explicitly instead of inferring targets by
  file existence, and absolutize relative `--output`/`--path` values the way
  `--config` already was, since the run `cd`s into the scratch mirror.
- Fail loudly on a nonexistent path instead of inheriting bare vale's fallback
  to stdin, which rendered a typo'd path as `0 errors ... in stdin`, exit 0 —
  a form the callers' `0 files` NOT-RUN guard cannot match.
- Follow symlinks when walking a directory argument, matching bare vale.

Refs: #85
Three divergences between what the audit skills claim and what the hooks
enforce, each of which fails silently rather than loudly:

- `skill-size-check.sh` blocked at 2900 words while `validate.sh` checked only
  the 500-line ceiling, so `/skill-audit` could report a skill ready to ship
  that the commit hook then rejected. `validate.sh` now checks the same pair on
  the same inclusive terms; the constants are duplicated with a comment naming
  the other file, because a plugin skill's scripts cannot read outside the
  plugin directory once installed to the cache.
- Both audit skills' Step 1 passed `--config assets/vale/.vale.ini`, which is
  redundant (the wrapper self-locates its sibling config) and fragile: an agent
  that resolves the script path against the skill directory but not the config
  path gets E100, exit 2, which the surrounding fallback clause misreads as
  "vale unavailable" and downgrades to full LLM judgment with no signal.
- The external-consumer test registered only the two Vale hooks, never the
  third shipped hook, so a lost executable bit would have broken every consumer
  while the local suite stayed green. Verified by mutation: `chmod 644` on the
  copied script now turns three passes into two failures.

Also corrects the size hook's calibration comment, which claimed ~5.7-6.5
characters per word against a corpus whose measured median is 6.79 — the stated
upper bound sat below the median, so the "calibrated with margin" claim was
inverted for prose-dense files. MAX_WORDS is unchanged pending a decision; the
comment is now explicit that the gate holds under 5,000 tokens for typical
prose density, not for any file.

Refs: #85
Four claims in shipped agent-facing docs did not match verified behaviour.
These are read as ground truth by agents in other repos, so each was
reproduced against vale 3.15.2 before rewriting:

- CONTEXT.md and `vale-config/SKILL.md` said both `>` and `|` block scalars
  break the description scope. `|` does not — it lints normally and fires every
  alert, while `>` yields zero. An agent following the old text would rewrite a
  working `|` description into a plain multi-line scalar, which genuinely does
  break, inverting the intended remediation. Both now name the forms that do
  break and state that `|` does not.
- CONTEXT.md and ADR-0013 described the size hook as failing only above 500
  lines, omitting the 2900-word gate it also enforces. Both now describe the
  pair and state that `validate.sh` checks the same two.
- ADR-0014 recorded an accepted residual — a wholesale `assets/` deletion going
  unflagged — that commit 14c2c91 closed. Left as the point-in-time record and
  amended with an update describing the union-with-tag-manifest mechanism,
  following the amendment precedent in ADR-0005.
- `vale-config/SKILL.md` asserted a fresh `.vale.ini` fails until `vale sync`
  runs, contradicting its own note that built-in styles need no download. The
  claim is now scoped to package styles; this repo's two configs declare no
  packages and lint clean with zero syncs.

Also repoints AGENTS.md at the seven `gitea:*` skills — the `bin:gitea` route
it named no longer exists.

Refs: #85
Author
Collaborator

Round 5 — review, independent validation, and fixes

Ran a full review of this branch, then a second independent pass that re-derived every empirical claim rather than trusting the first. All findings confirmed (no false positives), three regraded. Everything is now fixed in aa8cc22, 4ae2429, 57654c4.

The root cause was one rule, not four bugs

Vale locates a frontmatter description by matching the parsed YAML value back against the source text. Any scalar whose value isn't spelled out verbatim loses the text.frontmatter.description scope entirely. That single rule explains every symptom:

description form alerts on identical prose
single line 2
| literal 2
> folded 0
plain multi-line 0
double-quoted multi-line 0
single-quoted multi-line 0

The wrapper only flattened >. The other three broken forms passed silently with exit 0 — indistinguishable from a genuine clean pass, in a prefilter whose callers are told not to re-derive its verdict by judgment. No live instances in this repo (corpus is 32 folded / 19 single-line), so this was latent exposure via the shipped .pre-commit-hooks.yaml contract, not a regression.

This also corrected a factual error in three shipped docs: | literal blocks were documented as broken. They are not — line breaks survive, so the value stays verbatim-matchable. An agent following the old text would have rewritten a working | description into a plain multi-line scalar, which genuinely does break. The remediation was inverted.

Fixed

Wrapper (aa8cc22) — flattening covers all four broken forms via one shared continuation generator; | still untouched. The flattened value is now emitted in whichever scalar form needs no escape at all, because any escape re-breaks the verbatim match. That retires the blanket ' → U+2019 substitution, which had made apostrophe-bearing rule tokens unmatchable across 63% of the corpus. Note the obvious fix here does not work: '' doubling kills the scope outright — measured, not assumed. Also fixed in the same pass: continuations now terminate at a line flush with the key (a description: followed by a flush-left line previously swallowed the rest of the frontmatter); vale's value-taking flags are routed explicitly instead of targets being inferred by file existence (--output tmpl.tmpl was being linted as a target and reordering argv into a hard E100); relative --output/--path are absolutized like --config already was, since the run cds into the scratch mirror; symlinks are followed when walking a directory argument; and a nonexistent path now fails loudly instead of inheriting bare vale's fallback to stdin, which rendered a typo'd path as 0 errors … in stdin, exit 0 — a string the callers' 0 files NOT-RUN guard cannot match.

Audit/hook alignment (4ae2429) — validate.sh now enforces the same 500-line and 2900-word pair as skill-size-check.sh, so a skill can no longer pass its own audit and then be rejected by the commit hook. Both audit skills' Step 1 drops the redundant --config: the wrapper self-locates its sibling config, and an agent resolving the script path but not the config path got E100/exit 2, which the fallback clause misread as "vale unavailable" and silently downgraded to full LLM judgment. The external-consumer test now exercises all three shipped hooks — verified by mutation, chmod 644 on the copied script turns three passes into two failures.

Docs (57654c4) — the | correction above; both size ceilings documented in CONTEXT.md and ADR-0013; ADR-0014's "accepted residual" (wholesale assets/ deletion unflagged) marked closed by 14c2c91 with an amendment following ADR-0005's precedent rather than a silent rewrite; the vale sync self-contradiction in vale-config/SKILL.md scoped to package styles; and AGENTS.md repointed at the seven gitea:* skills, replacing a dead bin:gitea route.

Tests

tests/test-vale-wrap.sh goes 22 → 33 assertions. The new form-matrix case asserts each multi-line spelling reports exactly what its single-line spelling reports, each guarded by a check that bare vale reports nothing — so a silently-skipped flattening cannot pass vacuously. The pre-existing | test was strengthened the same way; it previously compared wrapper output to bare vale output and would have passed with both empty. New coverage also for the apostrophe token, the lossy-fallback liveness case, symlinks, separated flag values, and the typo'd path.

The bash-3.2 static check now scans skill-size-check.sh and check-release-needed.sh too. It flagged two lines in the latter; on inspection they are not hazards (RELEASE_PATHS is seeded non-empty at declaration and never reset, and the 3.2 abort only fires on an empty array), so rather than a name allowlist that goes stale silently, the exemption is encoded structurally.

Full suite green: 12 suites, 125 bats, 0 failures. All six pre-push gates pass. Live corpus clean — 39 SKILL.md and 6 agent files, 0 alerts — so the widened flattening surfaces nothing new in real files.

Two open items, neither blocking

  1. MAX_WORDS=2900 is calibrated to the corpus median with zero margin. Measured density is min 5.97 / median 6.79 / max 7.22 chars per word; at the densest observed ratio the gate permits ~5,240 tokens, above the 5,000 it proxies for. Holding the worst case under 5,000 needs ≈2770, which blocks nothing today (largest skill is 2,489 words). Left unchanged — tightening a blocking commit gate is a judgment call. The comment is now honest that the guarantee holds for typical prose density, not any file.
  2. CONTEXT.md has no size gate. Both hooks are scoped ^plugins/[^/]+/skills/[^/]+/SKILL\.md$. Caught during this round when an edit pushed CONTEXT.md to 2,905 words with nothing to flag it; trimmed to 2,816 by hand.

One residual is accepted by design: the lossy U+2019 fallback survives for the single combination no YAML scalar can carry verbatim (needs quoting and contains ' and contains " or \). It's documented at the emission site and covered by a test asserting the scope stays alive there.

🤖 Generated with Claude Code

## Round 5 — review, independent validation, and fixes Ran a full review of this branch, then a second independent pass that re-derived every empirical claim rather than trusting the first. All findings confirmed (no false positives), three regraded. Everything is now fixed in `aa8cc22`, `4ae2429`, `57654c4`. ### The root cause was one rule, not four bugs Vale locates a frontmatter description by **matching the parsed YAML value back against the source text**. Any scalar whose value isn't spelled out verbatim loses the `text.frontmatter.description` scope entirely. That single rule explains every symptom: | description form | alerts on identical prose | |---|---| | single line | 2 | | `\|` literal | 2 | | `>` folded | **0** | | plain multi-line | **0** | | double-quoted multi-line | **0** | | single-quoted multi-line | **0** | The wrapper only flattened `>`. The other three broken forms passed silently with exit 0 — indistinguishable from a genuine clean pass, in a prefilter whose callers are told not to re-derive its verdict by judgment. No live instances in this repo (corpus is 32 folded / 19 single-line), so this was latent exposure via the shipped `.pre-commit-hooks.yaml` contract, not a regression. This also corrected a **factual error in three shipped docs**: `|` literal blocks were documented as broken. They are not — line breaks survive, so the value stays verbatim-matchable. An agent following the old text would have rewritten a working `|` description into a plain multi-line scalar, which genuinely does break. The remediation was inverted. ### Fixed **Wrapper** (`aa8cc22`) — flattening covers all four broken forms via one shared continuation generator; `|` still untouched. The flattened value is now emitted in whichever scalar form needs *no escape at all*, because any escape re-breaks the verbatim match. That retires the blanket `'` → U+2019 substitution, which had made apostrophe-bearing rule tokens unmatchable across 63% of the corpus. Note the obvious fix here does not work: `''` doubling kills the scope outright — measured, not assumed. Also fixed in the same pass: continuations now terminate at a line flush with the key (a `description:` followed by a flush-left line previously swallowed the rest of the frontmatter); vale's value-taking flags are routed explicitly instead of targets being inferred by file existence (`--output tmpl.tmpl` was being linted as a target and reordering argv into a hard E100); relative `--output`/`--path` are absolutized like `--config` already was, since the run `cd`s into the scratch mirror; symlinks are followed when walking a directory argument; and a nonexistent path now fails loudly instead of inheriting bare vale's fallback to stdin, which rendered a typo'd path as `0 errors … in stdin`, exit 0 — a string the callers' `0 files` NOT-RUN guard cannot match. **Audit/hook alignment** (`4ae2429`) — `validate.sh` now enforces the same 500-line **and** 2900-word pair as `skill-size-check.sh`, so a skill can no longer pass its own audit and then be rejected by the commit hook. Both audit skills' Step 1 drops the redundant `--config`: the wrapper self-locates its sibling config, and an agent resolving the script path but not the config path got E100/exit 2, which the fallback clause misread as "vale unavailable" and silently downgraded to full LLM judgment. The external-consumer test now exercises all three shipped hooks — verified by mutation, `chmod 644` on the copied script turns three passes into two failures. **Docs** (`57654c4`) — the `|` correction above; both size ceilings documented in CONTEXT.md and ADR-0013; ADR-0014's "accepted residual" (wholesale `assets/` deletion unflagged) marked closed by `14c2c91` with an amendment following ADR-0005's precedent rather than a silent rewrite; the `vale sync` self-contradiction in `vale-config/SKILL.md` scoped to package styles; and AGENTS.md repointed at the seven `gitea:*` skills, replacing a dead `bin:gitea` route. ### Tests `tests/test-vale-wrap.sh` goes 22 → 33 assertions. The new form-matrix case asserts each multi-line spelling reports exactly what its single-line spelling reports, **each guarded by a check that bare vale reports nothing** — so a silently-skipped flattening cannot pass vacuously. The pre-existing `|` test was strengthened the same way; it previously compared wrapper output to bare vale output and would have passed with both empty. New coverage also for the apostrophe token, the lossy-fallback liveness case, symlinks, separated flag values, and the typo'd path. The bash-3.2 static check now scans `skill-size-check.sh` and `check-release-needed.sh` too. It flagged two lines in the latter; on inspection they are not hazards (`RELEASE_PATHS` is seeded non-empty at declaration and never reset, and the 3.2 abort only fires on an empty array), so rather than a name allowlist that goes stale silently, the exemption is encoded structurally. Full suite green: **12 suites, 125 bats, 0 failures**. All six pre-push gates pass. Live corpus clean — 39 `SKILL.md` and 6 agent files, 0 alerts — so the widened flattening surfaces nothing new in real files. ### Two open items, neither blocking 1. **`MAX_WORDS=2900` is calibrated to the corpus median with zero margin.** Measured density is min 5.97 / median 6.79 / max 7.22 chars per word; at the densest observed ratio the gate permits ~5,240 tokens, above the 5,000 it proxies for. Holding the worst case under 5,000 needs ≈2770, which blocks nothing today (largest skill is 2,489 words). Left unchanged — tightening a blocking commit gate is a judgment call. The comment is now honest that the guarantee holds for typical prose density, not any file. 2. **CONTEXT.md has no size gate.** Both hooks are scoped `^plugins/[^/]+/skills/[^/]+/SKILL\.md$`. Caught during this round when an edit pushed CONTEXT.md to 2,905 words with nothing to flag it; trimmed to 2,816 by hand. One residual is accepted by design: the lossy U+2019 fallback survives for the single combination no YAML scalar can carry verbatim (needs quoting **and** contains `'` **and** contains `"` or `\`). It's documented at the emission site and covered by a test asserting the scope stays alive there. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Defame1297 added 7 commits 2026-08-09 17:30:32 +00:00
The external-consumer test asserted a combined alert count (>=2) across both
shipped Vale hooks, but the SKILL.md fixture alone raises two alerts — so one
working hook satisfied the threshold. Retargeting agent-audit's glob to match
nothing left the suite reporting "3 passed" under the message "both hooks
flatten and flag". The Skipped guard does not catch this: the hook still
matches the file, Vale lints nothing, reports 0 errors in 1 file and exits 0,
which pre-commit renders as Passed. An assertion aggregating over N subjects
proves nothing about any individual subject.

Each hook now runs individually and its alerts are attributed to the nearest
preceding path header, so an alert is checked by path rather than by presence
in the combined blob. The two fixtures carry distinct VagueWording tokens, so
one hook's alert cannot be credited to another.

Nothing in the repo read either .vale.ini — the sync check diffed only
vale-wrap.sh and styles/Kyberforge, so a one-line glob typo silently disabled
the prefilter for a whole file type. That was the enabling half of the same
defect. The check now asserts the shared lines both copies must carry
(StylesPath, a section naming Kyberforge as a whole word) without flagging
their intentional divergence, and probes each glob section by asking Vale
itself to lint a representative path. Regex-to-glob comparison was rejected as
it means reimplementing doublestar semantics in bash; a file-count dry-run was
rejected because a section whose glob matches but whose BasedOnStyles lost
Kyberforge reports "1 file" with no alerts and would pass it.

Every new assertion is bound to a failing case in both directions: breaking the
artifact fails the suite, and neutering the assertion fails exactly one case.
That reverse sweep exposed two assertions bound to no failing case at all, one
masked by a stronger check running first.

Refs: #85
The flattener's last-resort branch rewrote ASCII ' to U+2019, justified as the
one combination no YAML scalar can carry verbatim. That claim was false: a |-
literal block with a single indented content line carries ', ", \ and ": "
verbatim and keeps text.frontmatter.description matching — as the wrapper's own
docstring already said of literal blocks. The rewrite fired on 12 of 54
in-scope files, silently disabling every rule whose token contains an
apostrophe. Case 20 pinned only that the scope stayed alive, so it passed
either way.

The emission site now splits the emitted scalar on its first newline so a
carried-over trailing comment stays on the "description: |-" header line rather
than becoming part of the value, and pads by span_lines - 1 - newlines. The pad
stays non-negative because the branch is only reachable when the original span
is at least two lines. Verified across all 73 in-scope files: no line-count
changes, and exactly the 12 expected files take the new branch.

One reported position moves: an alert on a description that is itself flagged
shifts from the key line to the block's content line, both inside the original
span. YAML cannot put a literal block's content on the key's own line, so this
is unavoidable; no line at or after the end of any description span moves.

Also: --output no longer absolutises the built-in style names line, JSON and
CLI, which a same-named file or directory in cwd turned into a template path
(exit 2, E100 Runtime error). And case 19's empty-baseline guard no longer
lets five dependent comparisons print vacuous passes — while fixing it the
guard turned out to be unreachable, since under pipefail an alert-free report
aborted the script at the assignment.

Refs: #85
ADR: 0014
The gate hardcoded HEAD as its diff tip, but pre-commit exports
PRE_COMMIT_TO_REF for exactly this. Pushing "somebranch:main" from another
checkout diffed the wrong tip — a false negative when HEAD is older, a false
positive when newer. Fixing only the diff tip leaves a second bug: git describe
took the tag baseline from HEAD too, so a tag reachable only from HEAD becomes
a baseline the pushed ref never saw. Both now resolve from the pushed ref, and
an all-zeros ref (branch deletion) short-circuits before any rev resolution
rather than surfacing as "could not diff".

PRE_COMMIT_FROM_REF is deliberately not used: it is the remote's current tip,
so diffing from it would let an untagged release-relevant commit already on
main excuse the next push from cutting a tag — the drift this gate exists to
catch. The baseline must stay the last release tag.

collect_release_paths took tokens[0] as a path unconditionally. ADR-0014 makes
bare single-path entries a binding constraint, but nothing enforced it, and the
sibling .pre-commit-config.yaml already ships "entry: bash <script>". Under
that shape add_release_path takes "bash", git diff accepts the non-matching
pathspec silently, bundle_root becomes "." and is skipped — the hook's whole
surface leaves the gate with no error, the same shape as the --config
regression in LESSONS.md. Multi-token entries now fail loudly naming the hook
and the ADR, and tokens[0] must resolve at HEAD or at the tag (the union is
load-bearing: a per-scope check would reject the deletion cases).

Six mutations verified, each restored. One correction worth recording: the
first multi-token test passed with its guard removed, because the existence
guard caught "bash" and printed a similar message. It now requires the verbatim
entry text that only the multi-token diagnostic emits.

Refs: #85
ADR: 0014
MAX_WORDS=2900 was calibrated to the corpus median density and carried no
margin: at the densest observed 7.22 chars/word (~1.81 tokens/word) it permits
~5,240 tokens against the 5,000 it proxies for. 2770 holds the worst observed
density under the ceiling. The largest SKILL.md is 2,489 words, so the change
costs nothing today — 281 words of margin — and the header comment now argues
the new calibration rather than swapping the digits.

Both enforcement points move together, and a new test asserts they agree, since
a SKILL.md passing its own audit while the commit hook blocks it is the
disagreement this pair exists to prevent.

CONTEXT.md is deliberately left ungated: it is 2,816 words, and gating it would
block the build. Recorded here so the omission reads as a decision rather than
an oversight.

skill-audit's manual-fallback path listed only the line ceiling, so an agent
taking that path passed an oversized SKILL.md the hook then rejected. The word
ceiling is now named alongside it. agent-audit is deliberately unchanged: the
size hook scopes to SKILL.md only and agent-audit's validate.sh has no word
gate, so claiming it there would be false.

The Vale research doc still showed the MDX {/* vale off */} form under a
Markdown heading, contradicting CONTEXT.md and vale-run's troubleshooting
reference — that form suppresses nothing in plain .md. Fixed in both places it
appeared.

tests/run-tests.sh used mapfile (bash 4.0+) with unguarded array expansion,
though AGENTS.md tells contributors to run it and macOS ships bash 3.2. It now
collects via a while-read loop over process substitution and guards every
expansion. The newline-delimited find|sort pipeline is kept rather than -print0
with sort -z, whose BSD portability is the weaker link, and which matches
mapfile -t's previous behaviour exactly.

Refs: #85
ADR: 0013
kyberforge 1.2.7 -> 1.2.8: both vale-wrap.sh copies, skill-audit's validate.sh
and its SKILL.md changed after the last bump. lint 1.1.4 -> 1.1.5: the Vale
research troubleshooting doc changed after its last bump.

Without the bump, installed copies keep serving the cached version. This is the
fourth time in this PR the bump was missed after shipped content changed —
check-manifests.sh validates parity between the two manifests but not that a
content change was accompanied by a bump, which is the gap that keeps letting
it through.

Refs: #85
The aggregate-assertion failure joins the "a clean result can mean nothing ran"
family as its fifth instance: a total over N subjects is satisfiable by a
proper subset, so it proves nothing about any individual subject. Records the
reverse mutation sweep — neuter each assertion, confirm exactly one case fails
— as standing practice for checks whose failure mode is silence.

The second entry is about accepted residuals: the U+2019 rewrite survived
review because its justification was documented in the same breath as the
workaround, and the covering test asserted the residual's presence rather than
the behaviour it cost. Documentation records a belief; a belief adjacent to a
workaround is the one most worth attacking.

Refs: #85
run_check set PRE_COMMIT_REMOTE_BRANCH and, when asked, PRE_COMMIT_TO_REF, but
never cleared what was already in the environment. Standalone that is invisible
— nothing sets those vars. Under the pre-push hook this suite exists to guard,
pre-commit exports PRE_COMMIT_TO_REF and PRE_COMMIT_FROM_REF as shas of the
real repo; the fixtures inherited them, the script resolved a rev that does not
exist in the fixture, and 13 of 20 cases failed. The suite passed in every
context except the only one that matters.

The variables are now cleared in both branches, so a standalone run and a
pre-push run are the same test. Verified 20/20 with the vars unset and with
them set to real shas of this repo.

Found by the pre-push hook rejecting the push, not by any test — the same shape
as the --config regression: the local invocation exercised a different thing
than the shipped one, and the two were indistinguishable by reading the file.

Refs: #85
Author
Collaborator

Round 6 — review findings addressed (57654c4..9a3f72b)

A full review of the branch (commit history + final state) produced 12 findings; each was then cross-checked against this thread before any code was written. Two did not survive that check and are recorded here rather than acted on:

  • Release-gate deadlock — rejected, correctly. The review claimed the zero-tag state hard-fails every push to main, and that tagging pre-merge is insufficient because "all paths are new relative to that tag". #1344's validator pass already rejected this, and I reproduced the rejection: git tag v1.0.0 at HEAD, then run the gate as if pushing to main → exit 0. Re-reporting it would have been noise. The genuine residues (no tags today; Gitea's merge button never runs the pre-push hook) remain documented at check-release-needed.sh:12-16, ADR-0014, and #1350.
  • The U+2019 fallback — half-wrong. "Undocumented anywhere" was false; it was documented at the emission site and covered by test 20, and accepted by design in #1355. But the rationale was falsifiable, and it turned out to be false — see below.

Fixed

# Sev Change
1+8 major The consumer test asserted a combined alert count (>=2) across both hooks, and the SKILL.md fixture alone raises two — so one working hook satisfied it. Retargeting agent-audit's glob to match nothing left the suite reporting 3 passed under the message "both hooks flatten and flag". Nothing read either .vale.ini — that was the enabling half. Alerts are now attributed per hook by path, with distinct trigger tokens per fixture, and the sync check probes each glob section by asking Vale to lint a representative path.
2 medium emit() no longer rewrites '→U+2019. A `
6+7 minor Release gate resolves both the tag baseline and the diff tip from PRE_COMMIT_TO_REF; multi-token entry: values now fail loudly instead of dropping a hook's whole surface from the gate.
4 minor kyberforge 1.2.7→1.2.8, lint 1.1.4→1.1.5.
5, 10, 11, 12 minor/nit Word ceiling added to skill-audit's manual-fallback list; stale MDX {/* vale off */} claim corrected; run-tests.sh made bash-3.2 safe; case-19's vacuous-pass guard closed.

Ceiling tightened 2900 → 2770 (open item 1 from #1355) in both enforcement points, now pinned by a parity assertion so they cannot drift. Largest SKILL.md is 2,489 words — 281 to spare.

CONTEXT.md deliberately left ungated (open item 2). At 2,816 words it sits above the new ceiling, so gating it would block the build. Recorded as a decision, not an oversight.

Two things worth flagging

The gate rejected its own push. After all six commits, git push failed: test-check-release-needed.sh passed standalone and in a full-suite run, but failed 13/20 under the pre-push hook. run_check set the PRE_COMMIT_* vars it needed but never cleared what was already in the environment — under pre-push, pre-commit exports refs of the real repo, the fixtures inherited them, and the script resolved a rev that does not exist in the fixture. The suite passed in every context except the only one that matters. Same shape as the --config regression: the local invocation exercised a different thing than the shipped one. Fixed in 9a3f72b; found by the hook, not by any test.

Reverse mutation sweeps are now standing practice. Beyond "break the artifact, confirm the suite fails", each new assertion was neutered in turn to confirm exactly one case fails. That exposed two assertions in check-vale-style-sync.sh bound to no failing case at all, one masked by a stronger check running first — and one release-gate test that passed with its guard removed, because a different guard produced a similar message.

Both patterns recorded in LESSONS.md (the aggregate-assertion one as the 5th instance of the "a clean result can mean nothing ran" family).

Full suite: 12 passed, 0 failed; check-manifests.sh and check-vale-style-sync.sh clean; wrapper copies byte-identical.

🤖 Generated with Claude Code

## Round 6 — review findings addressed (`57654c4..9a3f72b`) A full review of the branch (commit history + final state) produced 12 findings; each was then cross-checked against this thread before any code was written. **Two did not survive that check** and are recorded here rather than acted on: - **Release-gate deadlock — rejected, correctly.** The review claimed the zero-tag state hard-fails every push to `main`, and that tagging pre-merge is insufficient because "all paths are new relative to that tag". [#1344's validator pass already rejected this](#), and I reproduced the rejection: `git tag v1.0.0` at HEAD, then run the gate as if pushing to `main` → **exit 0**. Re-reporting it would have been noise. The genuine residues (no tags today; Gitea's merge button never runs the pre-push hook) remain documented at `check-release-needed.sh:12-16`, ADR-0014, and #1350. - **The U+2019 fallback — half-wrong.** "Undocumented anywhere" was false; it was documented at the emission site and covered by test 20, and accepted by design in #1355. But the *rationale* was falsifiable, and it turned out to be false — see below. ### Fixed | # | Sev | Change | |---|---|---| | 1+8 | **major** | The consumer test asserted a **combined** alert count (`>=2`) across both hooks, and the SKILL.md fixture alone raises two — so one working hook satisfied it. Retargeting agent-audit's glob to match nothing left the suite reporting `3 passed` under the message "both hooks flatten and flag". Nothing read either `.vale.ini` — that was the enabling half. Alerts are now attributed **per hook by path**, with distinct trigger tokens per fixture, and the sync check probes each glob section by asking Vale to lint a representative path. | | 2 | medium | `emit()` no longer rewrites `'`→U+2019. A `|-` literal block with one indented content line carries `'`, `"`, `\` and `: ` verbatim and keeps `text.frontmatter.description` matching — as the wrapper's own docstring already said of literal blocks. The rewrite was silently disabling apostrophe-bearing rules on **12 of 54** in-scope files. | | 6+7 | minor | Release gate resolves **both** the tag baseline and the diff tip from `PRE_COMMIT_TO_REF`; multi-token `entry:` values now fail loudly instead of dropping a hook's whole surface from the gate. | | 4 | minor | kyberforge `1.2.7→1.2.8`, lint `1.1.4→1.1.5`. | | 5, 10, 11, 12 | minor/nit | Word ceiling added to skill-audit's manual-fallback list; stale MDX `{/* vale off */}` claim corrected; `run-tests.sh` made bash-3.2 safe; case-19's vacuous-pass guard closed. | **Ceiling tightened `2900 → 2770`** (open item 1 from #1355) in both enforcement points, now pinned by a parity assertion so they cannot drift. Largest SKILL.md is 2,489 words — 281 to spare. **`CONTEXT.md` deliberately left ungated** (open item 2). At 2,816 words it sits above the new ceiling, so gating it would block the build. Recorded as a decision, not an oversight. ### Two things worth flagging **The gate rejected its own push.** After all six commits, `git push` failed: `test-check-release-needed.sh` passed standalone and in a full-suite run, but failed 13/20 under the pre-push hook. `run_check` set the `PRE_COMMIT_*` vars it needed but never cleared what was already in the environment — under pre-push, pre-commit exports refs of the *real* repo, the fixtures inherited them, and the script resolved a rev that does not exist in the fixture. The suite passed in every context except the only one that matters. Same shape as the `--config` regression: the local invocation exercised a different thing than the shipped one. Fixed in `9a3f72b`; found by the hook, not by any test. **Reverse mutation sweeps are now standing practice.** Beyond "break the artifact, confirm the suite fails", each new assertion was neutered in turn to confirm exactly one case fails. That exposed two assertions in `check-vale-style-sync.sh` bound to no failing case at all, one masked by a stronger check running first — and one release-gate test that passed with its guard removed, because a *different* guard produced a similar message. Both patterns recorded in `LESSONS.md` (the aggregate-assertion one as the 5th instance of the "a clean result can mean nothing ran" family). Full suite: **12 passed, 0 failed**; `check-manifests.sh` and `check-vale-style-sync.sh` clean; wrapper copies byte-identical. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Defame1297 added 3 commits 2026-08-09 20:00:26 +00:00
skill-audit's Description dimension implied Vale's DescriptionOpener
alert fully covers imperative-phrasing checks, but that rule only
matches the literal "This skill..." pattern. agent-audit kept its
equivalent manual "does the description open with a verb" fallback
bullet; skill-audit's got dropped when Vale wiring landed in #85,
leaving other non-imperative openers (gerunds, passive phrasing) to
sail through unflagged. Restore the parallel check.
Both READMEs' file tables predate #85's Vale wiring and never picked
up scripts/vale-wrap.sh or the assets/vale/ style tree, so a reader
of either README had no way to find where the new Step 1 sub-check
actually lives. List the new files and note the Vale sub-check in
"What it does" for both skills.
git describe --match is a shell glob, not a regex: the trailing `*`s
in 'v[0-9]*.[0-9]*.[0-9]*' match any suffix, so a tag like
v1.2.3-checkpoint or v1.2.3-rc1 satisfied the pattern and could be
picked as LAST_TAG instead of the true last release. That silently
shifts the diff baseline and can let a push skip a required release.
--exclude '*-*' rules out any tag carrying a hyphenated suffix.

Added a regression test that tags a release-relevant change with a
v1.0.1-checkpoint tag right after v1.0.0 and asserts the gate still
fires — confirmed it fails against the pre-fix script and passes
against the fix.
Author
Collaborator

Pre-merge review (round 4)

Ran four parallel review passes against the branch (verification gate, correctness/simplification/efficiency code review, security review, and a dogfooded skill-audit/agent-audit pass on the new/modified skill and agent files), spot-checked the higher-severity claims by hand, then fixed and pushed the confirmed findings.

Clean:

  • Full test suite: 12/12 files, 0 failed
  • All 17 pre-commit hooks pass, incl. both Vale prefilter hooks and skill-size-check
  • check-manifests.sh and claude plugin validate --strict clean on all 4 plugins
  • Security review: no injection/traversal/unsafe-eval findings — scripts are shellcheck-clean and consistently quoted
  • vale-config, vale-run, and the lint-runner agent pair: clean audits

Fixed in 0a41b2c, 050aec4, 6910f1b:

  1. fix(kyberforge) — skill-audit/SKILL.md had lost the manual "action-verb opening" fallback check that agent-audit/SKILL.md still has. Vale's DescriptionOpener rule only matches the literal This skill... pattern, so other non-imperative openers were going unflagged.
  2. docs(kyberforge) — skill-audit/README.md and agent-audit/README.md file tables predated this PR's Vale wiring and never listed scripts/vale-wrap.sh or the assets/vale/ style tree.
  3. fix(lint) — scripts/check-release-needed.sh's git describe --match 'v[0-9]*.[0-9]*.[0-9]*' is a shell glob, not a regex, so a tag like v1.2.3-checkpoint also satisfied it and could get picked as the release baseline instead of the true last release. Added --exclude '*-*' plus a regression test (test-check-release-needed.sh test 21) that fails against the old script and passes against the fix.

Reviewed and deliberately not changed (findings from the code-review pass that didn't hold up on inspection):

  • check-vale-style-sync.sh's regex-union approach for scope-coverage checking — the two pre-commit manifests' files: regexes differ by design (this repo's own layout vs. the generic layout shipped to external consumers), so unioning them is correct, not a masking bug.
  • run-tests.sh treating exit 77 as SKIPPED rather than FAILED — working as documented (automake convention), and the summary lists skipped scripts by name rather than hiding them.

Follow-up round: efficiency/reuse cleanups

Fixed the remaining low-severity findings from the code-review pass, in 680aa4f and e62f68a:

  1. refactor(kyberforge) — vale-wrap.sh's separated (--config X) and joined (--config=X) argument branches duplicated ~20 lines of path-absolutization logic; extracted into abs_config_value(). Also collapsed two python3 subprocess spawns into one for the common single-file case (flatten() now optionally does abspath + the flatten in the same process). Edited agent-audit's canonical copy, then regenerated skill-audit's copy via sync-vale-styles.sh — never hand-edited the second copy, to guarantee byte parity. No hardening (bash 3.2 compat, surrogateescape, symlink guards) touched. test-vale-wrap.sh 39/39.
  2. refactor(lint) — check-vale-style-sync.sh's hook_file_regexes() reparsed both pre-commit manifests on every call; the validation loop calls it 3 times (agent-audit twice, for its two file shapes), so agent-audit's regex set was being parsed twice for nothing. Now cached per skill in a lazily-populated associative array. test-check-vale-style-sync.sh 20/20.
  3. refactor(lint) — skill-size-check.sh read the target file twice (separate awk/wc -w calls) for line/word counts; now one awk pass. Also documented, next to the constants, why they're duplicated against skill-audit/scripts/validate.sh's Python implementation rather than unified (cross-language/cross-context tradeoff, same pattern as vale-wrap.sh, guarded by a drift test). test-skill-size-check.sh 9/9.

Investigated, no change made: the reported "missing source_keys" in agent-audit/references/README.md didn't hold up — it already has source_keys: [], matching an established, repeated convention in this plugin for pure-index reference files with no independent research content (e.g. agent-author/references/README.md and agent-author/references/scripts.md follow the same pattern). Populating it with unrelated slugs would have misattributed provenance. Leaving as-is.

Full suite (12/12) and all pre-commit/pre-push gates green after both pushes. Branch is clean and up to date. Recommend merging.

## Pre-merge review (round 4) Ran four parallel review passes against the branch (verification gate, correctness/simplification/efficiency code review, security review, and a dogfooded `skill-audit`/`agent-audit` pass on the new/modified skill and agent files), spot-checked the higher-severity claims by hand, then fixed and pushed the confirmed findings. **Clean:** - Full test suite: 12/12 files, 0 failed - All 17 pre-commit hooks pass, incl. both Vale prefilter hooks and `skill-size-check` - `check-manifests.sh` and `claude plugin validate --strict` clean on all 4 plugins - Security review: no injection/traversal/unsafe-eval findings — scripts are shellcheck-clean and consistently quoted - `vale-config`, `vale-run`, and the `lint-runner` agent pair: clean audits **Fixed in `0a41b2c`, `050aec4`, `6910f1b`:** 1. `fix(kyberforge)` — `skill-audit/SKILL.md` had lost the manual "action-verb opening" fallback check that `agent-audit/SKILL.md` still has. Vale's `DescriptionOpener` rule only matches the literal `This skill...` pattern, so other non-imperative openers were going unflagged. 2. `docs(kyberforge)` — `skill-audit/README.md` and `agent-audit/README.md` file tables predated this PR's Vale wiring and never listed `scripts/vale-wrap.sh` or the `assets/vale/` style tree. 3. `fix(lint)` — `scripts/check-release-needed.sh`'s `git describe --match 'v[0-9]*.[0-9]*.[0-9]*'` is a shell glob, not a regex, so a tag like `v1.2.3-checkpoint` also satisfied it and could get picked as the release baseline instead of the true last release. Added `--exclude '*-*'` plus a regression test (`test-check-release-needed.sh` test 21) that fails against the old script and passes against the fix. **Reviewed and deliberately not changed** (findings from the code-review pass that didn't hold up on inspection): - `check-vale-style-sync.sh`'s regex-union approach for scope-coverage checking — the two pre-commit manifests' `files:` regexes differ by design (this repo's own layout vs. the generic layout shipped to external consumers), so unioning them is correct, not a masking bug. - `run-tests.sh` treating exit 77 as SKIPPED rather than FAILED — working as documented (automake convention), and the summary lists skipped scripts by name rather than hiding them. --- ## Follow-up round: efficiency/reuse cleanups Fixed the remaining low-severity findings from the code-review pass, in `680aa4f` and `e62f68a`: 4. `refactor(kyberforge)` — `vale-wrap.sh`'s separated (`--config X`) and joined (`--config=X`) argument branches duplicated ~20 lines of path-absolutization logic; extracted into `abs_config_value()`. Also collapsed two `python3` subprocess spawns into one for the common single-file case (`flatten()` now optionally does `abspath` + the flatten in the same process). Edited `agent-audit`'s canonical copy, then regenerated `skill-audit`'s copy via `sync-vale-styles.sh` — never hand-edited the second copy, to guarantee byte parity. No hardening (bash 3.2 compat, surrogateescape, symlink guards) touched. `test-vale-wrap.sh` 39/39. 5. `refactor(lint)` — `check-vale-style-sync.sh`'s `hook_file_regexes()` reparsed both pre-commit manifests on every call; the validation loop calls it 3 times (agent-audit twice, for its two file shapes), so agent-audit's regex set was being parsed twice for nothing. Now cached per skill in a lazily-populated associative array. `test-check-vale-style-sync.sh` 20/20. 6. `refactor(lint)` — `skill-size-check.sh` read the target file twice (separate `awk`/`wc -w` calls) for line/word counts; now one `awk` pass. Also documented, next to the constants, why they're duplicated against `skill-audit/scripts/validate.sh`'s Python implementation rather than unified (cross-language/cross-context tradeoff, same pattern as `vale-wrap.sh`, guarded by a drift test). `test-skill-size-check.sh` 9/9. **Investigated, no change made:** the reported "missing `source_keys`" in `agent-audit/references/README.md` didn't hold up — it already has `source_keys: []`, matching an established, repeated convention in this plugin for pure-index reference files with no independent research content (e.g. `agent-author/references/README.md` and `agent-author/references/scripts.md` follow the same pattern). Populating it with unrelated slugs would have misattributed provenance. Leaving as-is. Full suite (12/12) and all pre-commit/pre-push gates green after both pushes. Branch is clean and up to date. Recommend merging.
Defame1297 added 2 commits 2026-08-09 20:17:24 +00:00
Two efficiency findings from a code-review pass:
- The separated (--config X) and joined (--config=X) argument branches
  duplicated ~20 lines of absolutize-if-relative path logic. Extracted
  into abs_config_value(), used by both branches; the redundant
  --config=/* special case falls out since the helper already passes
  absolute paths through unchanged.
- The common single-file path spawned python3 twice per file (once for
  abspath resolution, once for flatten()). flatten() now optionally
  takes a tmpdir arg and does both in one process. The per-file loop
  under a directory argument is unchanged — that path wasn't flagged.

agent-audit's copy is canonical; skill-audit's copy was regenerated via
scripts/sync-vale-styles.sh, not hand-edited, to guarantee byte parity.
No hardening (bash 3.2 compat, surrogateescape, symlink guards) touched.
Verified: tests/test-vale-wrap.sh 39/39, check-vale-style-sync.sh clean,
full suite 12/12.
Two more efficiency findings from the same code-review pass:

- check-vale-style-sync.sh's hook_file_regexes() reparsed both
  pre-commit manifests from scratch on every call. The final
  validation loop calls it once per probe (3 probes: skill-audit once,
  agent-audit twice for its two file shapes), so agent-audit's regex
  set was being parsed twice for no reason. Now cached per skill in a
  lazily-populated associative array, with a separate "seen" map so an
  empty result isn't mistaken for "not yet computed."
- skill-size-check.sh read the target file twice (separate awk and
  wc -w calls) to get line and word counts; now a single awk pass
  returns both. Also documented, next to MAX_LINES/MAX_WORDS, why
  those constants are duplicated against skill-audit/scripts/
  validate.sh's Python implementation rather than unified — same
  cross-language/cross-context tradeoff as vale-wrap.sh's duplication,
  guarded by tests/test-skill-size-check.sh's drift check.

Verified: test-check-vale-style-sync.sh 20/20, test-skill-size-check.sh
9/9, full suite 12/12, pre-commit --all-files clean.
Defame1297 added 2 commits 2026-08-10 07:40:58 +00:00
check-vale-style-sync.sh used `declare -A` for a per-skill regex cache.
Associative arrays are bash 4.0+; this script runs as an always-run
pre-push hook with `language: system`, so it inherits whatever bash is
first on the invoking user's PATH. On macOS's stock bash 3.2, `declare -A`
at top level aborts immediately under `set -euo pipefail` — every push
would hard-fail before the sync check ran anything.

Replaced with two parallel indexed arrays (HOOK_REGEX_CACHE_KEYS/_VALS),
linear-scanned by index — same caching behavior (avoids re-parsing both
pre-commit manifests when agent-audit is probed twice), but only ever
uses ${#arr[@]} and index access, never a bare ${arr[@]} expansion.

Extended test-vale-wrap.sh's existing bash-3.2 hazard sweep to scan this
file too, and added a check for `declare -A` itself — it previously only
caught unguarded ${arr[@]} expansions and mapfile/readarray, so this
exact regression had no test that would have caught it.

Refs: #85
gitea 1.3.2 -> 1.3.3. The round-1 Vale corpus fix (3324a73) changed
shipped skill content (gitea-issues, gitea-prs, gitea-releases SKILL.md)
without touching this plugin's manifests, so installed copies would keep
serving the old content from cache. Every other plugin whose content
changed in this PR got this bump (bin, kyberforge, lint, four times
total) — gitea was missed each time.

Marketplace entries carry no per-plugin version, so both marketplace.json
files are untouched, matching prior version-bump commits in this PR.

Refs: #85
Author
Collaborator

Independent review + fixes — 389a4f0 → 76e0df6

Ran three parallel review passes against the branch as it stood at e62f68a (verification of round-2's blocking items/gaps against the current tree, a fresh scripts/tests review, and a fresh docs/config/manifests review), then validated every finding against the full commit history and comment thread by hand before touching anything — a couple of the raw findings turned out to be wrong on inspection and were dropped rather than acted on.

Confirmed still fixed (round 2, re-verified by execution, not by reading the diff): the json.dumps escaping regression, the caveman/write-docs/tdd/etc. meaning-changing prose edits, the Step 1 cwd mismatch, and VagueQualifier's false-positive rate are all genuinely resolved. This branch has actually been through 6 review rounds, not the 3 the PR body's history table lists — that table is stale but it's a documentation nit, not a code issue.

Two real, previously-uncaught issues fixed here:

  1. 389a4f0 — check-vale-style-sync.sh used declare -A (bash 4.0+ associative arrays) for its per-skill regex cache. This script runs as an always-run pre-push hook with language: system, inheriting whatever bash is first on PATH — on macOS's stock bash 3.2 this aborts immediately under set -euo pipefail, hard-failing every push before the sync check does anything. Introduced by the PR's own most recent commit (e62f68a's caching refactor), after this repo's round-4 bash-3.2 sweep (#1350) had already verified the other three scripts clean — this one was never in that scanned list. Fixed with two parallel indexed arrays instead (same caching behavior, only ${#arr[@]}/index access, no bare ${arr[@]} expansion). Also extended test-vale-wrap.sh's hazard sweep to scan this file and to check for declare -A itself, since the existing sweep only caught unguarded ${arr[@]} and mapfile/readarray — this exact regression had no test that would have caught it.

  2. 76e0df6 — plugins/gitea/plugin.json / .claude-plugin/plugin.json were never version-bumped despite shipped content changes to gitea-issues, gitea-prs, and gitea-releases SKILL.md back in round 1 (3324a73). The identical bug class was independently caught and fixed four separate times elsewhere in this PR for bin/kyberforge/lint — gitea slipped through all four passes. Bumped 1.3.2 → 1.3.3; marketplace.json untouched per the established convention (no per-plugin version there).

Investigated and NOT changed (raised by the review passes, didn't hold up):

  • The "very different" → "fundamentally different" (prototype/SKILL.md) and "There is no auto-pagination" → "The MCP layer does no auto-pagination" / "completely" → "every page" (gitea-releases/SKILL.md) edits looked untraceable to any shipped rule at first pass. They aren't — both are genuine VagueQualifier/SentenceOpenerThereIs hits from before that rule was dropped. The "very different" one is even named explicitly in 149d564's commit message ("one marginal") as a deliberate keep, not an oversight.
  • The agent-audit pre-commit hook's files: regex only matching agents/*.md (missing bare *.agent.md elsewhere, e.g. assets/templates/copilot.agent.md) — consistent with round 1's explicit precedent of excluding template files from the local dev hook. Likely intentional, not a gap.

Gates: tests/run-tests.sh 12/12 (39/39 vale-wrap cases), check-manifests.sh clean, claude plugin validate --strict clean on all 6 plugins, full pre-push suite green on both commits.

🤖 Review + fixes via Claude Code

## Independent review + fixes — `389a4f0` → `76e0df6` Ran three parallel review passes against the branch as it stood at `e62f68a` (verification of round-2's blocking items/gaps against the current tree, a fresh scripts/tests review, and a fresh docs/config/manifests review), then validated every finding against the full commit history and comment thread by hand before touching anything — a couple of the raw findings turned out to be wrong on inspection and were dropped rather than acted on. **Confirmed still fixed (round 2, re-verified by execution, not by reading the diff):** the `json.dumps` escaping regression, the caveman/write-docs/tdd/etc. meaning-changing prose edits, the Step 1 cwd mismatch, and `VagueQualifier`'s false-positive rate are all genuinely resolved. This branch has actually been through 6 review rounds, not the 3 the PR body's history table lists — that table is stale but it's a documentation nit, not a code issue. **Two real, previously-uncaught issues fixed here:** 1. **`389a4f0`** — `check-vale-style-sync.sh` used `declare -A` (bash 4.0+ associative arrays) for its per-skill regex cache. This script runs as an always-run `pre-push` hook with `language: system`, inheriting whatever `bash` is first on PATH — on macOS's stock bash 3.2 this aborts immediately under `set -euo pipefail`, hard-failing every push before the sync check does anything. Introduced by the PR's own most recent commit (`e62f68a`'s caching refactor), after this repo's round-4 bash-3.2 sweep (`#1350`) had already verified the *other* three scripts clean — this one was never in that scanned list. Fixed with two parallel indexed arrays instead (same caching behavior, only `${#arr[@]}`/index access, no bare `${arr[@]}` expansion). Also extended `test-vale-wrap.sh`'s hazard sweep to scan this file and to check for `declare -A` itself, since the existing sweep only caught unguarded `${arr[@]}` and `mapfile`/`readarray` — this exact regression had no test that would have caught it. 2. **`76e0df6`** — `plugins/gitea/plugin.json` / `.claude-plugin/plugin.json` were never version-bumped despite shipped content changes to `gitea-issues`, `gitea-prs`, and `gitea-releases` SKILL.md back in round 1 (`3324a73`). The identical bug class was independently caught and fixed **four separate times** elsewhere in this PR for bin/kyberforge/lint — gitea slipped through all four passes. Bumped `1.3.2` → `1.3.3`; marketplace.json untouched per the established convention (no per-plugin version there). **Investigated and NOT changed** (raised by the review passes, didn't hold up): - The `"very different"` → `"fundamentally different"` (`prototype/SKILL.md`) and `"There is no auto-pagination"` → `"The MCP layer does no auto-pagination"` / `"completely"` → `"every page"` (`gitea-releases/SKILL.md`) edits looked untraceable to any shipped rule at first pass. They aren't — both are genuine `VagueQualifier`/`SentenceOpenerThereIs` hits from before that rule was dropped. The `"very different"` one is even named explicitly in `149d564`'s commit message ("one marginal") as a deliberate keep, not an oversight. - The agent-audit pre-commit hook's `files:` regex only matching `agents/*.md` (missing bare `*.agent.md` elsewhere, e.g. `assets/templates/copilot.agent.md`) — consistent with round 1's explicit precedent of excluding template files from the local dev hook. Likely intentional, not a gap. **Gates:** `tests/run-tests.sh` 12/12 (39/39 vale-wrap cases), `check-manifests.sh` clean, `claude plugin validate --strict` clean on all 6 plugins, full pre-push suite green on both commits. 🤖 Review + fixes via Claude Code
Defame1297 added 1 commit 2026-08-10 07:48:47 +00:00
A bad or stale REPO_ROOT argument fell through to the "neither copy
present" no-op guard and exited 0 — the exact "clean result can mean
nothing was checked" anti-pattern this PR spent multiple review rounds
eliminating elsewhere. That guard exists for a repo that legitimately has
no kyberforge plugin installed, not for a typo'd path.

Only the documented manual-invocation mode was affected: the shipped
pre-push hook always calls this script with zero args, which resolves via
`git rev-parse --show-toplevel` and is always valid inside a repo.

Added a regression test asserting a nonexistent REPO_ROOT exits non-zero.

Refs: #85
Author
Collaborator

Follow-up — cf5de2b, plus PR description corrected

cf5de2b — the third finding from my last pass, previously left as low-severity/optional: check-vale-style-sync.sh silently exited 0 when given a nonexistent REPO_ROOT argument, falling through to the "neither copy present" no-op guard rather than erroring. That guard exists for a repo legitimately missing the kyberforge plugin, not for a typo'd or stale path — same "clean result can mean nothing was checked" shape as several earlier findings in this thread. Now fails loudly with the bad path named in the message. Only the documented manual-invocation mode was affected; the shipped pre-push hook always calls with zero args, which resolves via git rev-parse --show-toplevel and was never at risk. Added a regression test (test-check-vale-style-sync.sh, now 21 cases).

PR description — updated the "Review history" section, which still said "Three rounds, all resolved" against what's actually 7 rounds of comments in this thread. Also corrected the "Deferred and tracked in ADR-0013: styles portability" line — that item was resolved back in round 4 (1164f3a) and formalized in ADR-0014, which explicitly supersedes ADR-0013's deferred-portability consequence; the PR body just hadn't caught up.

Gates: tests/run-tests.sh 12/12 (check-vale-style-sync.sh sub-suite 21/21), check-manifests.sh clean, full pre-push suite green on cf5de2b.

🤖 via Claude Code

## Follow-up — `cf5de2b`, plus PR description corrected **`cf5de2b`** — the third finding from my last pass, previously left as low-severity/optional: `check-vale-style-sync.sh` silently exited 0 when given a nonexistent `REPO_ROOT` argument, falling through to the "neither copy present" no-op guard rather than erroring. That guard exists for a repo legitimately missing the kyberforge plugin, not for a typo'd or stale path — same "clean result can mean nothing was checked" shape as several earlier findings in this thread. Now fails loudly with the bad path named in the message. Only the documented manual-invocation mode was affected; the shipped pre-push hook always calls with zero args, which resolves via `git rev-parse --show-toplevel` and was never at risk. Added a regression test (`test-check-vale-style-sync.sh`, now 21 cases). **PR description** — updated the "Review history" section, which still said "Three rounds, all resolved" against what's actually 7 rounds of comments in this thread. Also corrected the "Deferred and tracked in ADR-0013: styles portability" line — that item was resolved back in round 4 (`1164f3a`) and formalized in ADR-0014, which explicitly supersedes ADR-0013's deferred-portability consequence; the PR body just hadn't caught up. **Gates:** `tests/run-tests.sh` 12/12 (`check-vale-style-sync.sh` sub-suite 21/21), `check-manifests.sh` clean, full pre-push suite green on `cf5de2b`. 🤖 via Claude Code
Defame1297 approved these changes 2026-08-10 16:43:23 +00:00
Defame1297 merged commit 8f523da270 into main 2026-08-10 16:46:59 +00:00
Defame1297 deleted branch feat/84-vale-audit-prefilter 2026-08-10 16:46:59 +00:00
Sign in to join this conversation.