Round-3 review of PR #85 found the "enforcing" pre-commit hook enforced nothing. Vale's exit code keys on error-level alerts alone: five of the six rules were level: warning, so they exited 0, and pre-commit hides output from a passing hook — the alerts were invisible and blocked nothing. ADR-0013 rejected a report-only trial tier and then shipped one by accident. Flatten every rule to level: error. Vale's own exit code is then correct, so the hook entry drops to a bare vale-wrap.sh call and the graded error->FAIL / warning->SUGGESTION mapping disappears from both audit skills: every alert is a FAIL, in the gate and the audit alike. No ignorable tier, matching shellcheck, the test suite and conventional-pre-commit. Delete Kyberforge.VagueQualifier. Measured against the 41 skill/agent files as they stood before the rule ever ran: 2 hits. One marginal ("very different" -> "fundamentally different"), one an unfixable false positive — caveman/SKILL.md quotes "of course" as an example of filler, a mention not a use — which forced the only Vale suppression comments in the repo. Those four lines go with it; two of them were dead anyway, suppressing a frontmatter-scoped rule on a body line. Held-out prose (273 files) fired 15 times, 9 inside out-of-scope research examples and the rest one word in two idioms in a single doc. SentenceOpenerThereIs survives: 22 held-out hits, both in-corpus hits clean rewrites, zero suppressions. Widen .vale.ini's globs to [**/SKILL.md], [**/agents/*.md] and [**/*.agent.md]. The plugins/*/-prefixed globs scoped nothing — Vale's * crosses /, so they already matched docs/research/examples/**/agents/*.md and assets/templates/SKILL.md, the two paths CONTEXT.md claimed they excluded. Scoping is and was the hook's files: regex. The old globs also hid a silent false negative: a skill outside plugins/ matched no section, so Vale reported 0 files and exited 0, which both audits read as clean. They now treat a 0-file run as NOT RUN and fall back to full judgment. Also: - vale-wrap.sh resolves relative --config values and file arguments against the caller's cwd, as vale does, instead of the repo root, which hard-errored from a subdirectory and silently skipped flattening for file args that did not resolve from the root. Absolute paths inside the cwd are relativized so reports cite resolvable paths, not scratch ones. - vale-run's exit-code model was documented backwards ("exits non-zero whenever it finds an alert at or above MinAlertLevel") and would have led anyone following it to build a gate that passes everything. Its Markdown suppression syntax was MDX-only and does not suppress in .md; corrected in the skill and its troubleshooting reference, with backtick/fence exemption documented as the first resort. - skill-size-check.sh fails only above 500 lines, agreeing with skill-audit's validate.sh <= 500 pass. - ADR-0013 and CONTEXT.md amended to match, recording why graded severities cannot gate. Verified: 9 test scripts / 15 vale-wrap cases pass; vale-audit-prefilter, skill-size-check and shellcheck pass --all-files; check-manifests and claude plugin validate --strict clean. New tests fail against the old script (3 of them) and pass against the new one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
178 lines
6.8 KiB
Bash
Executable File
178 lines
6.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# Works around a Vale limitation: the `text.frontmatter.description` NLP scope
|
||
# silently stops matching once the `description:` value is a YAML block scalar
|
||
# (`>`/`|`) spanning 2+ physical lines — the style used by most skills/agents in
|
||
# this repo. Flattens the description to one physical line in a scratch copy
|
||
# (padding with blank lines so every other line number is unchanged), then runs
|
||
# the real `vale` binary against the copies. Drop-in replacement for calling
|
||
# `vale` directly: same args, same exit code.
|
||
#
|
||
# "Same args" means relative paths — `--config` values and file arguments alike
|
||
# — resolve against the caller's current directory, exactly as bare `vale`
|
||
# resolves them. (An earlier version resolved them against the repo root, an
|
||
# invented convention that hard-errored on `--config ../../.vale.ini` from a
|
||
# subdirectory and, worse, silently dropped file arguments that didn't happen to
|
||
# resolve from the repo root — skipping the flattening this script exists for.)
|
||
#
|
||
# Vale prints each file path exactly as it was handed to it, so the scratch tree
|
||
# mirrors the caller's absolute cwd: a relative file argument is passed through
|
||
# verbatim and resolves to its flattened copy, keeping the report byte-identical
|
||
# to bare `vale`'s. An absolute file argument inside the cwd is relativized to
|
||
# keep that property. Only an absolute path outside the cwd is rewritten to its
|
||
# scratch copy and so reports a scratch path — unavoidable, since a file can
|
||
# only be read from where it actually is.
|
||
|
||
cwd="$(pwd -P)"
|
||
|
||
vale_args=()
|
||
file_args=()
|
||
config_next=false
|
||
for arg in "$@"; do
|
||
if [[ "$config_next" == true ]]; then
|
||
config_next=false
|
||
if [[ "$arg" == /* ]]; then
|
||
vale_args+=("$arg")
|
||
else
|
||
vale_args+=("$cwd/$arg")
|
||
fi
|
||
continue
|
||
fi
|
||
case "$arg" in
|
||
--config)
|
||
vale_args+=("$arg")
|
||
config_next=true
|
||
continue
|
||
;;
|
||
--config=/*)
|
||
vale_args+=("$arg")
|
||
continue
|
||
;;
|
||
--config=*)
|
||
vale_args+=("--config=$cwd/${arg#--config=}")
|
||
continue
|
||
;;
|
||
esac
|
||
# `-f` resolves relative paths against the caller's cwd, same as vale does.
|
||
if [[ "$arg" != -* && -f "$arg" ]]; then
|
||
# An absolute path inside the caller's cwd is relativized so the report cites
|
||
# a path that resolves against the real tree. Left absolute, it would be
|
||
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
|
||
# path to a file that is deleted on exit, which reads as a bug in any report
|
||
# quoting it. Absolute paths outside the cwd have no relative form and keep
|
||
# the scratch-path behaviour documented above.
|
||
if [[ "$arg" == "$cwd"/* ]]; then
|
||
file_args+=("${arg#"$cwd"/}")
|
||
else
|
||
file_args+=("$arg")
|
||
fi
|
||
else
|
||
vale_args+=("$arg")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#file_args[@]} -eq 0 ]]; then
|
||
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
||
# block waiting on a pipe that will never carry content.
|
||
exec vale "${vale_args[@]}" < /dev/null
|
||
fi
|
||
|
||
tmpdir="$(realpath -m "$(mktemp -d)")"
|
||
trap 'rm -rf "$tmpdir"' EXIT
|
||
|
||
# Mirror of the caller's cwd inside the scratch tree; relative file arguments
|
||
# are resolved from here.
|
||
mirror="$tmpdir$cwd"
|
||
mkdir -p "$mirror"
|
||
|
||
argv_files=()
|
||
for arg in "${file_args[@]}"; do
|
||
if [[ "$arg" == /* ]]; then
|
||
dest="$tmpdir$arg"
|
||
else
|
||
dest="$mirror/$arg"
|
||
fi
|
||
dest="$(realpath -m "$dest")"
|
||
# A file argument with enough leading `..` to climb past the mirror root would
|
||
# write outside the scratch dir. The real filesystem clamps such a path at
|
||
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
|
||
case "$dest" in
|
||
"$tmpdir"/*) ;;
|
||
*)
|
||
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
|
||
exit 2
|
||
;;
|
||
esac
|
||
mkdir -p "$(dirname "$dest")"
|
||
python3 - "$arg" "$dest" <<'PYTHON'
|
||
import re
|
||
import sys
|
||
|
||
src, dest = sys.argv[1], sys.argv[2]
|
||
with open(src) as fh:
|
||
content = fh.read()
|
||
|
||
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
|
||
if fm_match:
|
||
fm = fm_match.group(2)
|
||
# Only `>`/`>-`/`>+` (folded) scalars break Vale's frontmatter-description
|
||
# scope. `|`/`|-`/`|+` (literal) scalars already work fine with bare vale,
|
||
# so they're deliberately left unmatched here.
|
||
header_m = re.search(r'^description:[ \t]*(>[+-]?)[ \t]*\n', fm, re.MULTILINE)
|
||
if header_m:
|
||
# Body capture is indentation-based and blank-line-tolerant, per YAML
|
||
# block-scalar rules: a blank line (any amount of whitespace) always
|
||
# stays inside the block; the indent is set by the first content line;
|
||
# the block ends at the first line indented less than that, or EOF.
|
||
rest = fm[header_m.end():]
|
||
indent = None
|
||
body_lines = []
|
||
for line in rest.splitlines(keepends=True):
|
||
text = line.rstrip('\n')
|
||
if text.strip() == '':
|
||
body_lines.append(line)
|
||
continue
|
||
line_indent = len(text) - len(text.lstrip(' \t'))
|
||
if indent is None:
|
||
indent = line_indent
|
||
elif line_indent < indent:
|
||
break
|
||
body_lines.append(line)
|
||
raw = ''.join(body_lines)
|
||
if raw.count('\n') >= 2:
|
||
flat = re.sub(r'\s+', ' ', raw).strip()
|
||
# YAML single-quoted scalars have no backslash-escape mechanism at
|
||
# all, so wrapping in single quotes sidesteps the backslash-escape
|
||
# bug entirely for embedded double quotes, backslashes, and
|
||
# non-ASCII text. The one YAML-spec-correct way to embed a literal
|
||
# apostrophe is to double it ('') — but Vale's own frontmatter
|
||
# scanner isn't a full YAML parser and doesn't understand that
|
||
# doubling: empirically, it silently truncates the value at the
|
||
# first ' it sees, hiding everything after it from the NLP scope
|
||
# (a different flavor of the same bug this whole script exists to
|
||
# work around). Since this copy is scratch-only and never written
|
||
# back, sidestep it by substituting a Unicode right single
|
||
# quotation mark (U+2019) for any literal apostrophe instead of
|
||
# doubling it — visually a smart quote, but never triggers a YAML
|
||
# escape sequence at all.
|
||
flat_q = "'" + flat.replace("'", "’") + "'"
|
||
pad = '\n' * raw.count('\n')
|
||
start = header_m.start()
|
||
end = header_m.end() + len(raw)
|
||
new_fm = fm[:start] + f'description: {flat_q}\n{pad}' + fm[end:]
|
||
content = fm_match.group(1) + new_fm + fm_match.group(3) + content[fm_match.end():]
|
||
|
||
with open(dest, 'w') as fh:
|
||
fh.write(content)
|
||
PYTHON
|
||
if [[ "$arg" == /* ]]; then
|
||
argv_files+=("$dest")
|
||
else
|
||
argv_files+=("$arg")
|
||
fi
|
||
done
|
||
|
||
cd "$mirror"
|
||
vale "${vale_args[@]}" "${argv_files[@]}"
|