#!/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[@]}"