From 0a9b07025525cec3ffc8d917561bb3f487fd66d8 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:19:21 -0500 Subject: [PATCH 01/14] Add design spec for Vale auto-fix rework Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-19-vale-autofix-design.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-19-vale-autofix-design.md diff --git a/docs/superpowers/specs/2026-03-19-vale-autofix-design.md b/docs/superpowers/specs/2026-03-19-vale-autofix-design.md new file mode 100644 index 0000000000..d5a9b6b79a --- /dev/null +++ b/docs/superpowers/specs/2026-03-19-vale-autofix-design.md @@ -0,0 +1,160 @@ +# Vale Auto-Fix Rework + +**Date:** 2026-03-19 +**Status:** Approved + +## Problem + +The current Vale linting flow reports violations but doesn't fix them: +- Pre-push hook warns about Vale issues (non-blocking) +- PR workflow posts up to 25 inline review comments + a summary table +- Authors must manually fix issues or ask `@claude` + +This creates friction. Writers don't need explanations for each error — they need the errors fixed. + +## Solution + +Replace the report-and-explain flow with an auto-fix flow: +1. Remove the pre-push hook entirely +2. Replace the PR workflow with a two-phase auto-fix workflow +3. Post a high-level summary comment listing fixes by category +4. No inline comments + +## Architecture + +### Two-Phase Auto-Fix + +**Phase 1 — Script fixes (deterministic):** +A bash script (`scripts/vale-autofix.sh`) handles 18 mechanical rules (~60% of all rules): + +Substitutions (12 rules): +- `drop-down` / `drop down` → `dropdown` +- `check box` → `checkbox` +- `click on` → `click` +- `in order to` → `to` +- `make sure` → `ensure` +- `utilize` → `use` +- Contractions (`do not` → `don't`, etc.) +- `is able to` → `can`, `was able to` → `could` +- `login to` → `log in to` +- `to setup` → `to set up` +- `provides the ability to` → `lets you` +- `wish to` → `want to` + +Removals (6 rules): +- `please` (in instruction context) +- Double spaces +- Temporal hedges (`currently`, `presently`, etc.) +- Latin abbreviations (`e.g.` → `for example`, etc.) +- Parenthetical plurals (`item(s)`) +- `aforementioned` + +The script: +- Takes a list of files as arguments +- Runs Vale in JSON mode to get violations with rule names and line numbers +- Only fixes violations Vale actually reported (not blind global sed) +- Outputs a JSON summary of fixes (rule name + count) + +**Phase 2 — Claude fixes (AI judgment):** +Claude Code GitHub Action handles 12 rules requiring context: +- First person rewrites (singular and plural) +- Weak link text +- Oxford comma insertion +- Idiom replacement +- Heading punctuation +- `allows you to` rewrites +- Boilerplate cross-references +- `note that` → admonition blocks +- `once` temporal usage → `After`/`When` +- `follow the steps to` rewrites +- Repeated words + +Claude: +- Receives remaining Vale violations (file, line, rule, message) +- Fixes each one in-place +- Skips any fix it's not confident about +- Outputs structured JSON of fixes and skips (rule + count + reason for skips) + +### Scope + +The workflow fixes **all Vale issues in every touched file**, not just issues on changed lines. This is simpler (no diff parsing) and improves files over time. + +### Workflow Structure + +New file: `.github/workflows/vale-autofix.yml` + +``` +Trigger: PR opened/synchronized targeting dev + paths: docs/**/*.md (excluding CLAUDE.md, SKILL.md, docs/kb/**) + +Permissions: contents write, pull-requests write + +Jobs: + vale-autofix: + steps: + 1. Checkout PR branch (with token that allows pushing) + 2. Get changed .md files (gh pr diff, same filtering as today) + 3. Exit early if no matching files + 4. Install Vale binary + 5. Run Vale JSON on changed files → violations.json + 6. Exit early if no violations + 7. Phase 1: Run scripts/vale-autofix.sh on violations.json + 8. Commit Phase 1 fixes (if any changes) + 9. Re-run Vale JSON on same files → remaining-violations.json + 10. Phase 2: Claude Code action with remaining violations as prompt + 11. Commit Phase 2 fixes (if any changes) + 12. Post/update summary comment +``` + +- Uses a PAT or GitHub App token (not `GITHUB_TOKEN`) so pushes trigger other workflows +- Bot identity commits with messages like `fix(vale): auto-fix substitutions` and `fix(vale): auto-fix rewrites` +- Replaces any previous Vale Auto-Fix comment on the same PR + +### Summary Comment Format + +```markdown +## Vale Auto-Fix Summary + +**22 issues fixed, 2 skipped across 3 files** + +| Category | Fixes | +|----------|-------| +| Substitutions (dropdown, checkbox, etc.) | 8 | +| Contractions (do not → don't, etc.) | 4 | +| Removed filler (please, currently, etc.) | 3 | +| Latin abbreviations (e.g. → for example, etc.) | 2 | +| Spacing | 1 | +| Rewrites (first person, weak link text, etc.) | 4 | + +| Skipped (needs manual review) | Reason | +|-------------------------------|--------| +| `docs/accessanalyzer/12.0/install.md:45` — Netwrix.WeakLinkText | Ambiguous link context | +| `docs/accessanalyzer/12.0/install.md:120` — Netwrix.Idioms | Multiple possible replacements | + +Ask `@claude` on this PR if you'd like an explanation of any fix. +``` + +## Files to Remove + +| File | Reason | +|------|--------| +| `.husky/pre-push` | Vale pre-push hook no longer needed | +| `.github/workflows/vale-linter.yml` | Replaced by `vale-autofix.yml` | + +## Files to Add + +| File | Purpose | +|------|---------| +| `scripts/vale-autofix.sh` | Deterministic Phase 1 script for mechanical Vale fixes | +| `.github/workflows/vale-autofix.yml` | Two-phase auto-fix workflow | + +## Files to Update + +| File | Changes | +|------|---------| +| `.claude/skills/doc-pr-fix/SKILL.md` | Remove all Vale fix logic (finding Vale comments, Vale fix tasks, Step 4b). Keep Dale and editorial fix logic. Remove "fix only the Vale issues" command. | +| `.claude/skills/doc-pr/SKILL.md` | Update Vale references: auto-fixed by `vale-autofix` workflow, not inline comments. Remove `@claude fix only the Vale issues` command. Update `@claude fix all issues` to cover Dale + editorial only. | +| `CLAUDE.md` | Update CI/CD workflows table: remove `vale-linter.yml`, add `vale-autofix.yml` | +| `CONTRIBUTING.md` | Remove Vale pre-push warning references, mention auto-fix on PRs | +| `README.md` | Remove Vale pre-push warning references if present | +| `docs/CLAUDE.md` | Update if it references Vale PR workflow | From 022ff5c10fc3b5efa347e306b14e7c604e8ab6c3 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:21:07 -0500 Subject: [PATCH 02/14] Add failure handling and concurrency to Vale auto-fix spec Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/superpowers/specs/2026-03-19-vale-autofix-design.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/superpowers/specs/2026-03-19-vale-autofix-design.md b/docs/superpowers/specs/2026-03-19-vale-autofix-design.md index d5a9b6b79a..0fc5bd259f 100644 --- a/docs/superpowers/specs/2026-03-19-vale-autofix-design.md +++ b/docs/superpowers/specs/2026-03-19-vale-autofix-design.md @@ -109,6 +109,8 @@ Jobs: - Uses a PAT or GitHub App token (not `GITHUB_TOKEN`) so pushes trigger other workflows - Bot identity commits with messages like `fix(vale): auto-fix substitutions` and `fix(vale): auto-fix rewrites` - Replaces any previous Vale Auto-Fix comment on the same PR +- **Phase 2 is best-effort** — if the Claude Code action fails (timeout, API error), the workflow still posts a summary of Phase 1 fixes and does not fail the job +- **Concurrency:** Uses a `concurrency` group keyed on the PR number with `cancel-in-progress: true` so rapid pushes don't cause racing workflow runs ### Summary Comment Format From 5f5d8dea520ee7ab0aec3e93893a0465a593341e Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:30:31 -0500 Subject: [PATCH 03/14] feat: add Vale auto-fix implementation plan 8-task plan covering script creation, workflow, cleanup, skill updates, and documentation updates. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-19-vale-autofix.md | 1037 +++++++++++++++++ 1 file changed, 1037 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-19-vale-autofix.md diff --git a/docs/superpowers/plans/2026-03-19-vale-autofix.md b/docs/superpowers/plans/2026-03-19-vale-autofix.md new file mode 100644 index 0000000000..f77a0fa5c6 --- /dev/null +++ b/docs/superpowers/plans/2026-03-19-vale-autofix.md @@ -0,0 +1,1037 @@ +# Vale Auto-Fix Rework Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the report-only Vale linting flow with a two-phase auto-fix workflow that commits fixes directly to PR branches and posts a high-level summary. + +**Architecture:** Phase 1 is a bash script that applies deterministic sed-based fixes for 18 mechanical Vale rules. Phase 2 invokes Claude Code GitHub Action for the 12 rules requiring AI judgment. A single GitHub Actions workflow orchestrates both phases, commits fixes, and posts a summary comment. + +**Tech Stack:** Bash (sed/jq), GitHub Actions, Claude Code GitHub Action (`anthropics/claude-code-action@v1`), Vale 3.9.5 + +**Spec:** `docs/superpowers/specs/2026-03-19-vale-autofix-design.md` + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|----------------| +| `scripts/vale-autofix.sh` | Create | Phase 1: deterministic sed-based fixes for 18 mechanical Vale rules | +| `.github/workflows/vale-autofix.yml` | Create | Orchestrates both phases, commits, posts summary comment | +| `.github/workflows/vale-linter.yml` | Delete | Replaced by vale-autofix.yml | +| `.husky/pre-push` | Delete | No longer needed | +| `.claude/skills/doc-pr-fix/SKILL.md` | Modify | Remove Vale fix logic, keep Dale/editorial | +| `.claude/skills/doc-pr/SKILL.md` | Modify | Update Vale references, remove Vale-only commands | +| `.github/workflows/claude-doc-pr.yml` | Modify | Remove Vale references in editorial review prompt and followup cleanup | +| `CLAUDE.md` | Modify | Update CI/CD table and workflow descriptions | +| `docs/CLAUDE.md` | Modify | Update Vale sections to reflect auto-fix flow | +| `CONTRIBUTING.md` | Modify | Remove pre-push Vale references, update workflow description | +| `README.md` | Modify | Remove pre-push Vale references, update project structure | + +--- + +### Task 1: Create the Phase 1 auto-fix script + +**Files:** +- Create: `scripts/vale-autofix.sh` + +This script reads Vale JSON output, applies deterministic fixes using sed, and outputs a JSON summary. + +- [ ] **Step 1: Create `scripts/vale-autofix.sh` with argument parsing and main loop** + +```bash +#!/usr/bin/env bash +# vale-autofix.sh — Phase 1: deterministic fixes for mechanical Vale rules +# Usage: vale-autofix.sh +# Output: writes fix summary to stdout as JSON + +set -euo pipefail + +VIOLATIONS_FILE="${1:?Usage: vale-autofix.sh }" + +if [ ! -f "$VIOLATIONS_FILE" ]; then + echo "[]" + exit 0 +fi + +TOTAL_FIXES=0 +declare -A FIX_COUNTS + +# Get unique files from violations +FILES=$(jq -r '[.[].path] | unique | .[]' "$VIOLATIONS_FILE") + +for FILE in $FILES; do + if [ ! -f "$FILE" ]; then + continue + fi + + # Build a set of line numbers inside fenced code blocks (``` or ~~~) + declare -A CODE_BLOCK_LINES + IN_FENCE=0 + LINENUM=0 + while IFS= read -r fline || [ -n "$fline" ]; do + LINENUM=$((LINENUM + 1)) + if echo "$fline" | grep -qE '^\s*(```|~~~)'; then + if [ "$IN_FENCE" -eq 0 ]; then + IN_FENCE=1 + else + IN_FENCE=0 + fi + CODE_BLOCK_LINES[$LINENUM]=1 + elif [ "$IN_FENCE" -eq 1 ]; then + CODE_BLOCK_LINES[$LINENUM]=1 + fi + done < "$FILE" + + # Get violations for this file, grouped by rule + RULES=$(jq -r --arg f "$FILE" '[.[] | select(.path == $f) | .check] | unique | .[]' "$VIOLATIONS_FILE") + + for RULE in $RULES; do + # Get line numbers for this rule in this file + LINES=$(jq -r --arg f "$FILE" --arg r "$RULE" '.[] | select(.path == $f and .check == $r) | .line' "$VIOLATIONS_FILE") + + FIXED=0 + for LINE_NUM in $LINES; do + # Skip lines inside fenced code blocks + if [ "${CODE_BLOCK_LINES[$LINE_NUM]:-0}" = "1" ]; then + continue + fi + + LINE_CONTENT=$(sed -n "${LINE_NUM}p" "$FILE") + + NEW_CONTENT="$LINE_CONTENT" + + case "$RULE" in + Netwrix.Checkbox) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/[Cc]heck [Bb]ox/checkbox/g') + ;; + Netwrix.ClickOn) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b([Dd]ouble-[Cc]lick|[Rr]ight-[Cc]lick|[Ll]eft-[Cc]lick|[Ll]eft [Cc]lick|[Cc]lick) [Oo]n\b/\1/g') + ;; + Netwrix.Contractions) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\bDo [Nn]ot\b/Don'\''t/g' \ + | sed -E 's/\bdo [Nn]ot\b/don'\''t/g' \ + | sed -E 's/\bDoes [Nn]ot\b/Doesn'\''t/g' \ + | sed -E 's/\bdoes [Nn]ot\b/doesn'\''t/g' \ + | sed -E 's/\bDid [Nn]ot\b/Didn'\''t/g' \ + | sed -E 's/\bdid [Nn]ot\b/didn'\''t/g' \ + | sed -E 's/\bCannot\b/Can'\''t/g' \ + | sed -E 's/\bcannot\b/can'\''t/g' \ + | sed -E 's/\bCan [Nn]ot\b/Can'\''t/g' \ + | sed -E 's/\bcan [Nn]ot\b/can'\''t/g' \ + | sed -E 's/\bWould [Nn]ot\b/Wouldn'\''t/g' \ + | sed -E 's/\bwould [Nn]ot\b/wouldn'\''t/g' \ + | sed -E 's/\bShould [Nn]ot\b/Shouldn'\''t/g' \ + | sed -E 's/\bshould [Nn]ot\b/shouldn'\''t/g' \ + | sed -E 's/\bCould [Nn]ot\b/Couldn'\''t/g' \ + | sed -E 's/\bcould [Nn]ot\b/couldn'\''t/g' \ + | sed -E 's/\bIs [Nn]ot\b/Isn'\''t/g' \ + | sed -E 's/\bis [Nn]ot\b/isn'\''t/g' \ + | sed -E 's/\bAre [Nn]ot\b/Aren'\''t/g' \ + | sed -E 's/\bare [Nn]ot\b/aren'\''t/g' \ + | sed -E 's/\bWas [Nn]ot\b/Wasn'\''t/g' \ + | sed -E 's/\bwas [Nn]ot\b/wasn'\''t/g' \ + | sed -E 's/\bWere [Nn]ot\b/Weren'\''t/g' \ + | sed -E 's/\bwere [Nn]ot\b/weren'\''t/g') + ;; + Netwrix.Dropdown) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/[Dd]rop[- ][Dd]own/dropdown/g') + ;; + Netwrix.InOrderTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/[Ii]n [Oo]rder [Tt]o/to/g') + ;; + Netwrix.IsAbleTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Ii]s [Aa]ble [Tt]o\b/can/g' \ + | sed -E 's/\b[Aa]re [Aa]ble [Tt]o\b/can/g' \ + | sed -E 's/\b[Ww]as [Aa]ble [Tt]o\b/could/g' \ + | sed -E 's/\b[Ww]ere [Aa]ble [Tt]o\b/could/g') + ;; + Netwrix.LoginVerb) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Ll]ogin [Tt]o\b/log in to/g') + ;; + Netwrix.MakeSure) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Mm]ake [Ss]ure\b/ensure/g') + ;; + Netwrix.ProvidesAbilityTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Pp]rovides the ability to\b/lets you/g' \ + | sed -E 's/\b[Pp]rovide the ability to\b/let you/g' \ + | sed -E 's/\b[Pp]rovided the ability to\b/let you/g') + ;; + Netwrix.SetupUsage) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b([Tt]o|[Ww]ill|[Cc]an|[Mm]ust|[Ss]hould|[Hh]ave|[Hh]as|[Hh]ad|[Gg]etting) [Ss]etup\b/\1 set up/g') + ;; + Netwrix.Utilize) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Uu]tiliz(es|ed|ing|e)\b/us\1/g' \ + | sed -E 's/\b[Uu]tilis(es|ed|ing|e)\b/us\1/g') + ;; + Netwrix.WishTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Ww]ish [Tt]o\b/want to/g') + ;; + Netwrix.Aforementioned) + # Remove "aforementioned" — replace with empty and clean up extra spaces + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Aa]forementioned\b *//g' | sed -E 's/ +/ /g') + ;; + Netwrix.LatinAbbreviations) + # Case-sensitive replacements + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\be\.g\./for example/g' \ + | sed -E 's/\bi\.e\./that is/g' \ + | sed -E 's/\betc\./and so on/g') + ;; + Netwrix.Please) + # Remove "please" but not "please note" (handled by NoteThat rule) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Pp]lease ([^n])/\1/g; s/\b[Pp]lease$//g' | sed -E 's/ +/ /g') + ;; + Netwrix.Spacing) + # Replace multiple spaces after sentence-ending punctuation with single space + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/([.!?]) +/\1 /g') + ;; + Netwrix.TemporalHedges) + # Remove temporal hedges and clean up + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Cc]urrently,? *//g' \ + | sed -E 's/\b[Pp]resently,? *//g' \ + | sed -E 's/\b[Aa]s of this writing,? *//g' \ + | sed -E 's/ +/ /g') + ;; + Netwrix.Plurals) + # Remove parenthetical plurals: "item(s)" → "items" + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/(\w+)\(s\)/\1s/g') + ;; + *) + # Unknown rule — skip + continue + ;; + esac + + if [ "$NEW_CONTENT" != "$LINE_CONTENT" ]; then + # Escape sed special characters in both old and new content for safe replacement + ESCAPED_OLD=$(printf '%s\n' "$LINE_CONTENT" | sed 's/[&/\]/\\&/g') + ESCAPED_NEW=$(printf '%s\n' "$NEW_CONTENT" | sed 's/[&/\]/\\&/g') + sed -i "${LINE_NUM}s|.*|${ESCAPED_NEW}|" "$FILE" + FIXED=$((FIXED + 1)) + fi + done + + if [ "$FIXED" -gt 0 ]; then + KEY="$RULE" + FIX_COUNTS[$KEY]=$(( ${FIX_COUNTS[$KEY]:-0} + FIXED )) + TOTAL_FIXES=$((TOTAL_FIXES + FIXED)) + fi + done + unset CODE_BLOCK_LINES +done + +# Map rules to human-readable categories for the summary comment +declare -A CATEGORY_MAP=( + ["Netwrix.Checkbox"]="Substitutions" + ["Netwrix.ClickOn"]="Substitutions" + ["Netwrix.Dropdown"]="Substitutions" + ["Netwrix.InOrderTo"]="Substitutions" + ["Netwrix.IsAbleTo"]="Substitutions" + ["Netwrix.LoginVerb"]="Substitutions" + ["Netwrix.MakeSure"]="Substitutions" + ["Netwrix.ProvidesAbilityTo"]="Substitutions" + ["Netwrix.SetupUsage"]="Substitutions" + ["Netwrix.Utilize"]="Substitutions" + ["Netwrix.WishTo"]="Substitutions" + ["Netwrix.Contractions"]="Contractions" + ["Netwrix.Please"]="Removed filler" + ["Netwrix.TemporalHedges"]="Removed filler" + ["Netwrix.Aforementioned"]="Removed filler" + ["Netwrix.LatinAbbreviations"]="Latin abbreviations" + ["Netwrix.Spacing"]="Spacing" + ["Netwrix.Plurals"]="Plurals" +) + +# Aggregate by category +declare -A CATEGORY_COUNTS +for KEY in "${!FIX_COUNTS[@]}"; do + CAT="${CATEGORY_MAP[$KEY]:-Other}" + CATEGORY_COUNTS[$CAT]=$(( ${CATEGORY_COUNTS[$CAT]:-0} + FIX_COUNTS[$KEY] )) +done + +# Output JSON summary grouped by category +echo "{" +echo " \"total\": $TOTAL_FIXES," +echo " \"by_category\": {" +FIRST=true +for CAT in "${!CATEGORY_COUNTS[@]}"; do + if [ "$FIRST" = true ]; then + FIRST=false + else + echo "," + fi + printf ' "%s": %d' "$CAT" "${CATEGORY_COUNTS[$CAT]}" +done +echo "" +echo " }" +echo "}" +``` + +- [ ] **Step 2: Make the script executable** + +Run: `chmod +x scripts/vale-autofix.sh` + +- [ ] **Step 3: Test the script locally on a sample file** + +Create a temporary test file with known violations and run: + +```bash +# Create test file +cat > /tmp/test-vale.md << 'TESTEOF' +Make sure you check box the option in the drop-down menu. +You do not need to click on the button. +Please utilize the setup wizard in order to configure. +The aforementioned feature is currently available. +This is e.g. an example. Extra spaces here. +The user(s) can login to the system. +The system provides the ability to export reports. +If you wish to proceed, the system is able to generate item(s). +TESTEOF + +# Run vale on test file to get violations JSON +vale --output JSON /tmp/test-vale.md 2>/dev/null | jq '[to_entries[] | .value[] | {path: "/tmp/test-vale.md", line: .Line, check: .Check, message: .Message}]' > /tmp/test-violations.json + +# Run the script +./scripts/vale-autofix.sh /tmp/test-violations.json + +# Verify the fixes +cat /tmp/test-vale.md +``` + +Expected: All mechanical violations fixed. Script outputs JSON with fix counts. + +- [ ] **Step 4: Commit** + +```bash +git add scripts/vale-autofix.sh +git commit -m "feat: add Phase 1 Vale auto-fix script + +Deterministic sed-based fixes for 18 mechanical Vale rules: +12 substitution rules and 6 removal rules." +``` + +--- + +### Task 2: Create the vale-autofix GitHub Actions workflow + +**Files:** +- Create: `.github/workflows/vale-autofix.yml` + +- [ ] **Step 1: Create the workflow file** + +```yaml +name: Vale Auto-Fix + +on: + pull_request: + types: [opened, synchronize] + branches: + - dev + paths: + - 'docs/**/*.md' + - '!docs/**/CLAUDE.md' + - '!docs/**/SKILL.md' + - '!docs/kb/**' + +concurrency: + group: vale-autofix-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + vale-autofix: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.PAT_TOKEN }} + fetch-depth: 0 + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Get changed markdown files + id: changed-files + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + CHANGED_MD_FILES=$(gh pr diff "$PR_NUMBER" --name-only | grep -E '^docs/.*\.md$' | grep -v '/CLAUDE\.md$' | grep -v '/SKILL\.md$' | grep -v '^docs/kb/' || true) + if [ -z "$CHANGED_MD_FILES" ]; then + echo "No docs markdown files changed" + echo "count=0" >> "$GITHUB_OUTPUT" + else + echo "Changed markdown files:" + echo "$CHANGED_MD_FILES" + echo "count=$(echo "$CHANGED_MD_FILES" | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT" + echo "$CHANGED_MD_FILES" > /tmp/changed-files.txt + fi + + - name: Install Vale + if: steps.changed-files.outputs.count > 0 + run: | + wget -q https://github.com/errata-ai/vale/releases/download/v3.9.5/vale_3.9.5_Linux_64-bit.tar.gz -O /tmp/vale.tar.gz + tar -xzf /tmp/vale.tar.gz -C /tmp + chmod +x /tmp/vale + /tmp/vale --version + + - name: Run Vale on changed files + id: vale-initial + if: steps.changed-files.outputs.count > 0 + run: | + jq -n '[]' > /tmp/violations.json + + while IFS= read -r file; do + if [ -f "$file" ]; then + RESULT=$(/tmp/vale --output JSON "$file" 2>/dev/null || true) + if [ -n "$RESULT" ] && [ "$RESULT" != "{}" ]; then + echo "$RESULT" | jq --arg f "$file" ' + [to_entries[] | .value[] | {path: $f, line: .Line, check: .Check, message: .Message}] + ' > /tmp/vale-file.json + jq -s '.[0] + .[1]' /tmp/violations.json /tmp/vale-file.json > /tmp/vale-tmp.json + mv /tmp/vale-tmp.json /tmp/violations.json + fi + fi + done < /tmp/changed-files.txt + + TOTAL=$(jq 'length' /tmp/violations.json) + echo "total=$TOTAL" >> "$GITHUB_OUTPUT" + echo "Vale found $TOTAL issue(s)" + + - name: Phase 1 — Script fixes + id: phase1 + if: steps.vale-initial.outputs.total > 0 + run: | + chmod +x scripts/vale-autofix.sh + SUMMARY=$(./scripts/vale-autofix.sh /tmp/violations.json) + echo "$SUMMARY" > /tmp/phase1-summary.json + PHASE1_TOTAL=$(echo "$SUMMARY" | jq '.total') + echo "fixed=$PHASE1_TOTAL" >> "$GITHUB_OUTPUT" + echo "Phase 1 fixed $PHASE1_TOTAL issue(s)" + + - name: Commit Phase 1 fixes + id: phase1-commit + if: steps.phase1.outputs.fixed > 0 + run: | + if git diff --quiet; then + echo "committed=false" >> "$GITHUB_OUTPUT" + else + git add -A docs/ + git commit -m "fix(vale): auto-fix substitutions and removals" + git push + echo "committed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Re-run Vale for remaining violations + id: vale-remaining + if: steps.vale-initial.outputs.total > 0 + run: | + jq -n '[]' > /tmp/remaining-violations.json + + while IFS= read -r file; do + if [ -f "$file" ]; then + RESULT=$(/tmp/vale --output JSON "$file" 2>/dev/null || true) + if [ -n "$RESULT" ] && [ "$RESULT" != "{}" ]; then + echo "$RESULT" | jq --arg f "$file" ' + [to_entries[] | .value[] | {path: $f, line: .Line, check: .Check, message: .Message}] + ' > /tmp/vale-file.json + jq -s '.[0] + .[1]' /tmp/remaining-violations.json /tmp/vale-file.json > /tmp/vale-tmp.json + mv /tmp/vale-tmp.json /tmp/remaining-violations.json + fi + fi + done < /tmp/changed-files.txt + + REMAINING=$(jq 'length' /tmp/remaining-violations.json) + echo "remaining=$REMAINING" >> "$GITHUB_OUTPUT" + echo "$REMAINING remaining violation(s) for Phase 2" + + - name: Phase 2 — Claude fixes + id: phase2 + if: steps.vale-remaining.outputs.remaining > 0 + continue-on-error: true + uses: anthropics/claude-code-action@v1 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + show_full_output: true + prompt: | + You are a documentation fixer. Your job is to fix Vale linting violations in markdown files. + + Read docs/CLAUDE.md for Netwrix writing standards before making any changes. + + Here are the remaining Vale violations after mechanical fixes were applied: + + ```json + $(cat /tmp/remaining-violations.json) + ``` + + For each violation: + 1. Read the file and understand the context around the flagged line + 2. Apply a fix that resolves the Vale rule while preserving the author's meaning + 3. If you are NOT confident in a fix (ambiguous context, multiple valid interpretations, fix would change meaning), SKIP it + + After fixing, write a JSON summary to /tmp/phase2-summary.json with this structure: + ```json + { + "fixed": [ + {"path": "file.md", "line": 1, "check": "Netwrix.RuleName", "action": "brief description of fix"} + ], + "skipped": [ + {"path": "file.md", "line": 1, "check": "Netwrix.RuleName", "reason": "why it was skipped"} + ] + } + ``` + + After writing the summary, stage and commit your changes: + ```bash + git add -A docs/ + git commit -m "fix(vale): auto-fix rewrites (AI-assisted)" + git push + ``` + + IMPORTANT: Write the summary JSON file BEFORE committing. Do not post any PR comments. + claude_args: '--allowedTools "Bash(git:*),Read,Write,Edit,Glob,Grep"' + + - name: Build and post summary comment + if: steps.vale-initial.outputs.total > 0 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + REPO=${{ github.repository }} + + # Collect Phase 1 stats + PHASE1_TOTAL=0 + PHASE1_BODY="" + if [ -f /tmp/phase1-summary.json ]; then + PHASE1_TOTAL=$(jq '.total' /tmp/phase1-summary.json) + # Build category rows from Phase 1 + PHASE1_BODY=$(jq -r ' + .by_category | to_entries | sort_by(.key) | .[] | + "| \(.key) | \(.value) |" + ' /tmp/phase1-summary.json) + fi + + # Collect Phase 2 stats + PHASE2_FIXED=0 + PHASE2_SKIPPED=0 + PHASE2_BODY="" + SKIPPED_BODY="" + if [ -f /tmp/phase2-summary.json ]; then + PHASE2_FIXED=$(jq '.fixed | length' /tmp/phase2-summary.json) + PHASE2_SKIPPED=$(jq '.skipped | length' /tmp/phase2-summary.json) + + if [ "$PHASE2_FIXED" -gt 0 ]; then + PHASE2_BODY=$(jq -r ' + [.fixed[] | .check] | group_by(.) | .[] | + "| \(.[0] | sub("Netwrix\\."; "")) (rewrite) | \(length) |" + ' /tmp/phase2-summary.json) + fi + + if [ "$PHASE2_SKIPPED" -gt 0 ]; then + SKIPPED_BODY=$(jq -r ' + .skipped[] | + "| `\(.path):\(.line)` — \(.check) | \(.reason) |" + ' /tmp/phase2-summary.json) + fi + fi + + TOTAL_FIXED=$((PHASE1_TOTAL + PHASE2_FIXED)) + TOTAL_SKIPPED=$PHASE2_SKIPPED + + # Count affected files + FILE_COUNT=$(wc -l < /tmp/changed-files.txt | tr -d ' ') + + # Build the summary comment + { + echo "## Vale Auto-Fix Summary" + echo "" + echo "**${TOTAL_FIXED} issues fixed, ${TOTAL_SKIPPED} skipped across ${FILE_COUNT} files**" + echo "" + + if [ "$TOTAL_FIXED" -gt 0 ]; then + echo "| Category | Fixes |" + echo "|----------|-------|" + if [ -n "$PHASE1_BODY" ]; then + echo "$PHASE1_BODY" + fi + if [ -n "$PHASE2_BODY" ]; then + echo "$PHASE2_BODY" + fi + echo "" + fi + + if [ "$TOTAL_SKIPPED" -gt 0 ]; then + echo "| Skipped (needs manual review) | Reason |" + echo "|-------------------------------|--------|" + echo "$SKIPPED_BODY" + echo "" + fi + + echo 'Ask `@claude` on this PR if you'\''d like an explanation of any fix.' + } > /tmp/vale-summary.md + + # Delete previous Vale Auto-Fix comments + COMMENT_IDS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | contains("## Vale Auto-Fix Summary"))) | .id] | .[]' 2>/dev/null || true) + for ID in $COMMENT_IDS; do + gh api "repos/${REPO}/issues/comments/${ID}" -X DELETE 2>/dev/null || true + done + + # Post new summary + gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file /tmp/vale-summary.md +``` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/vale-autofix.yml +git commit -m "feat: add Vale auto-fix workflow + +Two-phase auto-fix: script fixes for mechanical rules, then +Claude for AI-judgment rules. Posts summary comment on PR." +``` + +--- + +### Task 3: Remove the old Vale linting infrastructure + +**Files:** +- Delete: `.github/workflows/vale-linter.yml` +- Delete: `.husky/pre-push` + +- [ ] **Step 1: Delete the old workflow file** + +```bash +rm .github/workflows/vale-linter.yml +``` + +- [ ] **Step 2: Delete the pre-push hook** + +```bash +rm .husky/pre-push +``` + +- [ ] **Step 3: Commit** + +```bash +git add -A .github/workflows/vale-linter.yml .husky/pre-push +git commit -m "remove: delete old Vale linter workflow and pre-push hook + +Replaced by vale-autofix.yml which auto-fixes issues instead of +reporting them." +``` + +--- + +### Task 4: Update the doc-pr-fix skill + +**Files:** +- Modify: `.claude/skills/doc-pr-fix/SKILL.md` + +Remove all Vale-specific logic. Vale issues are now auto-fixed by the workflow, so this skill only handles Dale and editorial fixes. + +- [ ] **Step 1: Update the skill description** + +In `.claude/skills/doc-pr-fix/SKILL.md`, change the frontmatter description from: + +``` +description: "Autonomous fixer for documentation PRs. Triggered by @claude comments on PRs targeting dev. Reads the writer's request, the doc-pr review comment, and the Vale linting comment, then applies fixes and commits. Use this skill whenever a writer tags @claude on a documentation PR — not for interactive help (use doc-help for that), but for autonomous, single-shot fixes in CI." +``` + +to: + +``` +description: "Autonomous fixer for documentation PRs. Triggered by @claude comments on PRs targeting dev. Reads the writer's request and the doc-pr review comment, then applies fixes and commits. Use this skill whenever a writer tags @claude on a documentation PR — not for interactive help (use doc-help for that), but for autonomous, single-shot fixes in CI." +``` + +- [ ] **Step 2: Update Step 1 (Understand the request)** + +Remove these two patterns from the list: +- `- **Fix all issues** — apply every fix from the doc-pr review comment and the Vale linting comment` +- `- **Fix only Vale issues** — apply only fixes from the Vale linting comment` + +Replace the "Fix all issues" line with: +- `- **Fix all issues** — apply every fix from the doc-pr review comment (Dale + editorial)` + +- [ ] **Step 3: Update Step 2 (Gather context)** + +Remove step 4 entirely (the one that finds the Vale linting comment via `gh api`): +``` +4. If the writer asks to fix Vale issues (or "all issues"), also find the Vale linting comment: + ```bash + gh api repos/{owner}/{repo}/issues/$PR_NUMBER/comments --jq '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("## Vale Linting"))) | .body' | tail -1 + ``` + This gives you the Vale results table with file paths, line numbers, and rule violations. +``` + +- [ ] **Step 4: Update Step 3 (Plan your work)** + +Remove Vale task examples from the task list: +- Remove `- Fix Vale issues in \`path/to/file.md\` (N issues)` +- Remove `- Fix Vale issues in \`path/to/other.md\` (N issues)` + +Do the same in the progress comment example and the final summary example in Step 7. + +Also update the scope note: change `If they said "fix only the Dale issues," your task list should contain Dale fixes, verify, and commit — no Vale tasks, no editorial tasks.` to `If they said "fix only the Dale issues," your task list should contain Dale fixes, verify, and commit — no editorial tasks.` + +- [ ] **Step 5: Update Step 4 (Apply fixes)** + +Remove the Vale-specific bullet: +- `- For **Vale fixes**: read \`docs/CLAUDE.md\` for Vale guidance (especially the two rules requiring extra care), then fix each flagged issue in order, file by file` + +Update the scope note at the bottom: change `Only change what was requested; don't fix other categories of issues even if they're on the same line (e.g., if asked to fix Vale issues, don't also fix Dale or editorial issues)` to `Only change what was requested; don't fix other categories of issues even if they're on the same line (e.g., if asked to fix Dale issues, don't also fix editorial issues)` + +- [ ] **Step 6: Remove Step 4b entirely** + +Delete the entire "Step 4b: Update the Vale Idioms rule" section (lines 98-109 of the current file). The idiom rule still gets updated, but only through Dale's `[idiom]` tagging — it doesn't need a Vale-specific step anymore since Vale issues are auto-fixed. + +- [ ] **Step 7: Update the Behavioral Notes** + +Change the last bullet: `"fix the Vale issues," only fix Vale issues — don't also fix Dale issues, editorial issues` to `"fix the Dale issues," only fix Dale issues — don't also fix editorial issues` + +- [ ] **Step 8: Commit** + +```bash +git add .claude/skills/doc-pr-fix/SKILL.md +git commit -m "update: remove Vale fix logic from doc-pr-fix skill + +Vale issues are now auto-fixed by the vale-autofix workflow. +This skill now handles only Dale and editorial fixes." +``` + +--- + +### Task 5: Update the doc-pr skill + +**Files:** +- Modify: `.claude/skills/doc-pr/SKILL.md` + +- [ ] **Step 1: Update the skill description** + +Change the frontmatter description. Remove references to Vale inline comments and the vale-linter workflow. Update to mention that Vale issues are auto-fixed separately. + +- [ ] **Step 2: Update the Vale reference paragraph** + +Change line 11: +``` +Vale linting runs separately (via the vale-linter workflow) and posts inline review comments plus a summary PR comment. Do not run Vale or include Vale results in your review. +``` +to: +``` +Vale issues are auto-fixed separately by the vale-autofix workflow. Do not run Vale or include Vale results in your review. +``` + +- [ ] **Step 3: Update the "What to do next" section in the output template** + +Remove these two lines from the `@claude` command examples: +- `- \`@claude fix all issues\` — fix all Vale, Dale, and editorial issues` +- `- \`@claude fix only the Vale issues\` — fix just the Vale issues` + +Replace with: +- `- \`@claude fix all issues\` — fix all Dale and editorial issues` + +- [ ] **Step 4: Update the summary line** + +Change: +``` +N Dale issues, N editorial suggestions across N files. Vale issues are posted in a separate comment by the vale-linter workflow. +``` +to: +``` +N Dale issues, N editorial suggestions across N files. Vale issues are auto-fixed separately. +``` + +- [ ] **Step 5: Commit** + +```bash +git add .claude/skills/doc-pr/SKILL.md +git commit -m "update: remove Vale inline comment references from doc-pr skill + +Vale issues are now auto-fixed by the vale-autofix workflow." +``` + +--- + +### Task 6: Update the claude-doc-pr workflow + +**Files:** +- Modify: `.github/workflows/claude-doc-pr.yml` + +- [ ] **Step 1: Update the editorial review prompt** + +In the editorial review step prompt, change: +``` +NOTE: Vale linting runs separately (vale-linter workflow) and posts inline review comments plus a summary PR comment. Do not run Vale or include Vale issues in this review. +``` +to: +``` +NOTE: Vale issues are auto-fixed separately by the vale-autofix workflow. Do not run Vale or include Vale issues in this review. +``` + +- [ ] **Step 2: Verify the "What to do next" section** + +The editorial review prompt's output template already only references Dale and editorial issues (no Vale commands). Verify this is the case — no changes needed if confirmed. The `@claude fix all issues` command in this workflow context already means Dale + editorial only. + +- [ ] **Step 3: Update the followup cleanup step** + +In the "Resolve inline comments and dismiss old reviews" step, remove Vale-related cleanup. The `select` filter currently checks for both `**Dale**` and `**Vale**` — remove the Vale part: + +Change: +``` +select(.isResolved == false and ((.comments.nodes[0].body | contains("**Dale**")) or (.comments.nodes[0].body | contains("**Vale**")))) +``` +to: +``` +select(.isResolved == false and (.comments.nodes[0].body | contains("**Dale**"))) +``` + +Similarly for review dismissals, remove the Vale check: +Change: +``` +select(.user.login == "github-actions[bot]" and ((.body | contains("Dale found")) or (.body | contains("Vale found")))) +``` +to: +``` +select(.user.login == "github-actions[bot]" and (.body | contains("Dale found"))) +``` + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/claude-doc-pr.yml +git commit -m "update: remove Vale references from doc-pr workflow + +Vale is now handled by vale-autofix workflow." +``` + +--- + +### Task 7: Update project documentation + +**Files:** +- Modify: `CLAUDE.md` +- Modify: `docs/CLAUDE.md` +- Modify: `CONTRIBUTING.md` +- Modify: `README.md` + +- [ ] **Step 1: Update `CLAUDE.md` CI/CD table** + +In the CI/CD Workflows table, replace the `vale-linter.yml` row: +``` +| `vale-linter.yml` | PRs with `.md` changes | Vale inline review comments (up to 25) + summary PR comment | +``` +with: +``` +| `vale-autofix.yml` | PRs with `.md` changes | Auto-fix Vale issues (script + AI), post summary comment | +``` + +- [ ] **Step 2: Update `docs/CLAUDE.md` Vale sections** + +Replace the "Vale (pre-push)" section under "Linting": +``` +### Vale (pre-push) + +Vale runs via a pre-push hook that checks changed `docs/*.md` files and reports errors. The hook currently runs in warning mode — it shows errors but does not block the push. Always run Vale locally and fix all errors before pushing. +``` +with: +``` +### Vale + +Vale enforces 30 Netwrix-specific rules in `.vale/styles/Netwrix/` covering word choice, punctuation, formatting, and common writing issues. Vale issues are auto-fixed on PRs by the `vale-autofix` workflow — you don't need to fix them manually before pushing. You can still run Vale locally to preview issues: +``` + +Update the "CI/CD Context" section. Replace: +``` +**Vale (pre-push hook)** — Runs automatically before every push. Currently in warning mode — shows errors but does not block the push. Writers should fix all Vale errors locally before pushing. + +**Vale (PR review)** — Runs on PRs to `dev` with docs changes. Posts up to 25 inline review comments on changed lines plus a summary PR comment. +``` +with: +``` +**Vale (auto-fix)** — Runs on PRs to `dev` with docs changes. Automatically fixes Vale issues in two phases (script for mechanical rules, Claude for judgment-based rules) and posts a summary comment. No inline comments. +``` + +Update the "Common Mistakes" section. Remove: +``` +- Don't ignore Vale warnings before pushing — fix errors to keep documentation clean +``` + +- [ ] **Step 3: Update `CONTRIBUTING.md`** + +Change the Vale prerequisite description (line 10): +``` +- **Vale** (style linter — required for pushing documentation changes) +``` +to: +``` +- **Vale** (style linter — optional for local use; issues are auto-fixed on PRs) +``` + +Update the Install Vale intro paragraph (line 14): +``` +[Vale](https://vale.sh/) is a command-line linter for prose. It checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale is required to push changes to documentation files. The pre-push hook runs Vale automatically and blocks pushes that have linting errors. +``` +to: +``` +[Vale](https://vale.sh/) is a command-line linter for prose. It checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale issues are auto-fixed on PRs, but you can install it locally to preview issues before pushing. +``` + +Update the workflow steps (lines 57-65). Change step 3 from: +``` +3. Run Vale on your changed files to catch linting errors. +``` +to: +``` +3. Optionally run Vale on your changed files to preview issues (they'll be auto-fixed on the PR). +``` + +Change step 5 from: +``` +5. Push your branch (the pre-push hook runs Vale automatically and warns about any errors). +``` +to: +``` +5. Push your branch. +``` + +Update the PR description paragraph (line 65): +``` +After you open a PR, an automated review runs Vale linting, Dale linting (AI-powered), and an editorial review, then posts the results as PR comments. If you want help applying the suggested fixes, comment `@claude` on the PR followed by your request and Claude will apply fixes and push a commit. If Claude needs clarification, it will ask in the PR comments. +``` +to: +``` +After you open a PR, Vale issues are auto-fixed and a summary is posted. Dale linting (AI-powered) and an editorial review also run and post results as PR comments. To get help with Dale or editorial suggestions, comment `@claude` on the PR followed by your request. +``` + +Update the "Linting with Vale" section (lines 93-107). Change the intro: +``` +Vale enforces 30 Netwrix-specific rules covering word choice, punctuation, formatting, and common writing issues. The pre-push hook runs Vale automatically and warns about errors in changed `docs/*.md` files. + +Run Vale on a file before pushing to catch issues early: +``` +to: +``` +Vale enforces 30 Netwrix-specific rules covering word choice, punctuation, formatting, and common writing issues. Vale issues are auto-fixed on PRs, but you can run it locally to preview: +``` + +Update the "Common Mistakes" section. Change: +``` +- Don't ignore Vale warnings before pushing — fix errors to keep documentation clean. +``` +to: +``` +- Vale issues are auto-fixed on PRs, but running Vale locally helps catch issues early. +``` + +- [ ] **Step 4: Update `README.md`** + +Change the Vale prerequisite (line 31): +``` +- **Vale** (style linter — required for pushing documentation changes) +``` +to: +``` +- **Vale** (style linter — optional for local use; issues are auto-fixed on PRs) +``` + +Update the Install Vale intro (line 35): +``` +[Vale](https://vale.sh/) is a command-line linter for prose. A linter checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale is required to push changes to documentation files. The pre-push hook runs Vale automatically and blocks pushes that have linting errors. +``` +to: +``` +[Vale](https://vale.sh/) is a command-line linter for prose. A linter checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale issues are auto-fixed on PRs, but you can install it locally to preview issues before pushing. +``` + +Update the project structure tree. Change: +``` +├── .husky/ +│ └── pre-push # Vale pre-push hook (blocks on lint errors) +``` +to: +``` +├── .husky/ # Git hooks (managed by Husky) +``` + +Update the "Run Vale Locally" section intro (lines 75-87). Change: +``` +### Run Vale Locally + +Run Vale on a file before pushing to catch issues early: +``` +to: +``` +### Run Vale Locally + +Vale issues are auto-fixed on PRs, but you can run Vale locally to preview: +``` + +- [ ] **Step 5: Commit** + +```bash +git add CLAUDE.md docs/CLAUDE.md CONTRIBUTING.md README.md +git commit -m "docs: update all references to reflect Vale auto-fix flow + +Vale issues are now auto-fixed on PRs. Pre-push hook removed. +Inline comments removed. Updated CLAUDE.md, docs/CLAUDE.md, +CONTRIBUTING.md, and README.md." +``` + +--- + +### Task 8: End-to-end verification + +- [ ] **Step 1: Verify all old Vale references are cleaned up** + +```bash +# Search for stale references to vale-linter or pre-push Vale +grep -r "vale-linter" --include="*.yml" --include="*.md" --include="*.json" . +grep -r "pre-push" --include="*.md" . +grep -r "Vale inline" --include="*.md" . +grep -r "Vale.*inline comment" --include="*.md" . +``` + +Expected: No results (or only the spec/plan files). + +- [ ] **Step 2: Verify deleted files don't exist** + +```bash +ls .github/workflows/vale-linter.yml 2>&1 # Should say "No such file" +ls .husky/pre-push 2>&1 # Should say "No such file" +``` + +- [ ] **Step 3: Verify new files exist and are valid** + +```bash +ls scripts/vale-autofix.sh # Should exist +ls .github/workflows/vale-autofix.yml # Should exist +bash -n scripts/vale-autofix.sh # Syntax check +``` + +- [ ] **Step 4: Verify the auto-fix script runs without errors on a clean repo** + +```bash +echo '[]' > /tmp/empty-violations.json +./scripts/vale-autofix.sh /tmp/empty-violations.json +# Should output: {"total": 0, "by_rule": {}} +``` + +- [ ] **Step 5: Spot-check updated files for consistency** + +Read through each updated file to verify no stale Vale references remain and all new text is consistent. From 5651ca304f170397461c6129259ec7ab46cbc3d4 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:32:10 -0500 Subject: [PATCH 04/14] fix: address plan review issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use awk instead of sed for line replacement (avoids | delimiter collision in markdown tables) - Fix expected output typo (by_rule → by_category) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/superpowers/plans/2026-03-19-vale-autofix.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-03-19-vale-autofix.md b/docs/superpowers/plans/2026-03-19-vale-autofix.md index f77a0fa5c6..470d237deb 100644 --- a/docs/superpowers/plans/2026-03-19-vale-autofix.md +++ b/docs/superpowers/plans/2026-03-19-vale-autofix.md @@ -210,10 +210,8 @@ for FILE in $FILES; do esac if [ "$NEW_CONTENT" != "$LINE_CONTENT" ]; then - # Escape sed special characters in both old and new content for safe replacement - ESCAPED_OLD=$(printf '%s\n' "$LINE_CONTENT" | sed 's/[&/\]/\\&/g') - ESCAPED_NEW=$(printf '%s\n' "$NEW_CONTENT" | sed 's/[&/\]/\\&/g') - sed -i "${LINE_NUM}s|.*|${ESCAPED_NEW}|" "$FILE" + # Use awk for safe line replacement (avoids sed delimiter issues with | in markdown tables) + awk -v n="$LINE_NUM" -v new="$NEW_CONTENT" 'NR==n{print new;next}1' "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE" FIXED=$((FIXED + 1)) fi done @@ -1029,7 +1027,7 @@ bash -n scripts/vale-autofix.sh # Syntax check ```bash echo '[]' > /tmp/empty-violations.json ./scripts/vale-autofix.sh /tmp/empty-violations.json -# Should output: {"total": 0, "by_rule": {}} +# Should output: {"total": 0, "by_category": {}} ``` - [ ] **Step 5: Spot-check updated files for consistency** From 3d97de3ded9175511c759353a2c5feb0fd0f2267 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:35:39 -0500 Subject: [PATCH 05/14] feat: add Phase 1 Vale auto-fix script Deterministic sed-based fixes for 18 mechanical Vale rules: 12 substitution rules and 6 removal rules. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/vale-autofix.sh | 222 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100755 scripts/vale-autofix.sh diff --git a/scripts/vale-autofix.sh b/scripts/vale-autofix.sh new file mode 100755 index 0000000000..afc3461631 --- /dev/null +++ b/scripts/vale-autofix.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +# vale-autofix.sh — Phase 1: deterministic fixes for mechanical Vale rules +# Usage: vale-autofix.sh +# Output: writes fix summary to stdout as JSON + +set -euo pipefail + +VIOLATIONS_FILE="${1:?Usage: vale-autofix.sh }" + +if [ ! -f "$VIOLATIONS_FILE" ]; then + echo "[]" + exit 0 +fi + +TOTAL_FIXES=0 +declare -A FIX_COUNTS + +# Get unique files from violations +FILES=$(jq -r '[.[].path] | unique | .[]' "$VIOLATIONS_FILE") + +for FILE in $FILES; do + if [ ! -f "$FILE" ]; then + continue + fi + + # Build a set of line numbers inside fenced code blocks (``` or ~~~) + declare -A CODE_BLOCK_LINES + IN_FENCE=0 + LINENUM=0 + while IFS= read -r fline || [ -n "$fline" ]; do + LINENUM=$((LINENUM + 1)) + if echo "$fline" | grep -qE '^\s*(```|~~~)'; then + if [ "$IN_FENCE" -eq 0 ]; then + IN_FENCE=1 + else + IN_FENCE=0 + fi + CODE_BLOCK_LINES[$LINENUM]=1 + elif [ "$IN_FENCE" -eq 1 ]; then + CODE_BLOCK_LINES[$LINENUM]=1 + fi + done < "$FILE" + + # Get violations for this file, grouped by rule + RULES=$(jq -r --arg f "$FILE" '[.[] | select(.path == $f) | .check] | unique | .[]' "$VIOLATIONS_FILE") + + for RULE in $RULES; do + # Get line numbers for this rule in this file + LINES=$(jq -r --arg f "$FILE" --arg r "$RULE" '.[] | select(.path == $f and .check == $r) | .line' "$VIOLATIONS_FILE") + + FIXED=0 + for LINE_NUM in $LINES; do + # Skip lines inside fenced code blocks + if [ "${CODE_BLOCK_LINES[$LINE_NUM]:-0}" = "1" ]; then + continue + fi + + LINE_CONTENT=$(sed -n "${LINE_NUM}p" "$FILE") + + NEW_CONTENT="$LINE_CONTENT" + + case "$RULE" in + Netwrix.Checkbox) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/[Cc]heck [Bb]ox/checkbox/g') + ;; + Netwrix.ClickOn) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b([Dd]ouble-[Cc]lick|[Rr]ight-[Cc]lick|[Ll]eft-[Cc]lick|[Ll]eft [Cc]lick|[Cc]lick) [Oo]n\b/\1/g') + ;; + Netwrix.Contractions) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\bDo [Nn]ot\b/Don'\''t/g' \ + | sed -E 's/\bdo [Nn]ot\b/don'\''t/g' \ + | sed -E 's/\bDoes [Nn]ot\b/Doesn'\''t/g' \ + | sed -E 's/\bdoes [Nn]ot\b/doesn'\''t/g' \ + | sed -E 's/\bDid [Nn]ot\b/Didn'\''t/g' \ + | sed -E 's/\bdid [Nn]ot\b/didn'\''t/g' \ + | sed -E 's/\bCannot\b/Can'\''t/g' \ + | sed -E 's/\bcannot\b/can'\''t/g' \ + | sed -E 's/\bCan [Nn]ot\b/Can'\''t/g' \ + | sed -E 's/\bcan [Nn]ot\b/can'\''t/g' \ + | sed -E 's/\bWould [Nn]ot\b/Wouldn'\''t/g' \ + | sed -E 's/\bwould [Nn]ot\b/wouldn'\''t/g' \ + | sed -E 's/\bShould [Nn]ot\b/Shouldn'\''t/g' \ + | sed -E 's/\bshould [Nn]ot\b/shouldn'\''t/g' \ + | sed -E 's/\bCould [Nn]ot\b/Couldn'\''t/g' \ + | sed -E 's/\bcould [Nn]ot\b/couldn'\''t/g' \ + | sed -E 's/\bIs [Nn]ot\b/Isn'\''t/g' \ + | sed -E 's/\bis [Nn]ot\b/isn'\''t/g' \ + | sed -E 's/\bAre [Nn]ot\b/Aren'\''t/g' \ + | sed -E 's/\bare [Nn]ot\b/aren'\''t/g' \ + | sed -E 's/\bWas [Nn]ot\b/Wasn'\''t/g' \ + | sed -E 's/\bwas [Nn]ot\b/wasn'\''t/g' \ + | sed -E 's/\bWere [Nn]ot\b/Weren'\''t/g' \ + | sed -E 's/\bwere [Nn]ot\b/weren'\''t/g') + ;; + Netwrix.Dropdown) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/[Dd]rop[- ][Dd]own/dropdown/g') + ;; + Netwrix.InOrderTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/[Ii]n [Oo]rder [Tt]o/to/g') + ;; + Netwrix.IsAbleTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Ii]s [Aa]ble [Tt]o\b/can/g' \ + | sed -E 's/\b[Aa]re [Aa]ble [Tt]o\b/can/g' \ + | sed -E 's/\b[Ww]as [Aa]ble [Tt]o\b/could/g' \ + | sed -E 's/\b[Ww]ere [Aa]ble [Tt]o\b/could/g') + ;; + Netwrix.LoginVerb) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Ll]ogin [Tt]o\b/log in to/g') + ;; + Netwrix.MakeSure) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Mm]ake [Ss]ure\b/ensure/g') + ;; + Netwrix.ProvidesAbilityTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Pp]rovides the ability to\b/lets you/g' \ + | sed -E 's/\b[Pp]rovide the ability to\b/let you/g' \ + | sed -E 's/\b[Pp]rovided the ability to\b/let you/g') + ;; + Netwrix.SetupUsage) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b([Tt]o|[Ww]ill|[Cc]an|[Mm]ust|[Ss]hould|[Hh]ave|[Hh]as|[Hh]ad|[Gg]etting) [Ss]etup\b/\1 set up/g') + ;; + Netwrix.Utilize) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Uu]tiliz(es|ed|ing|e)\b/us\1/g' \ + | sed -E 's/\b[Uu]tilis(es|ed|ing|e)\b/us\1/g') + ;; + Netwrix.WishTo) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Ww]ish [Tt]o\b/want to/g') + ;; + Netwrix.Aforementioned) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Aa]forementioned\b *//g' | sed -E 's/ +/ /g') + ;; + Netwrix.LatinAbbreviations) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\be\.g\./for example/g' \ + | sed -E 's/\bi\.e\./that is/g' \ + | sed -E 's/\betc\./and so on/g') + ;; + Netwrix.Please) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Pp]lease ([^n])/\1/g; s/\b[Pp]lease$//g' | sed -E 's/ +/ /g') + ;; + Netwrix.Spacing) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/([.!?]) +/\1 /g') + ;; + Netwrix.TemporalHedges) + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Cc]urrently,? *//g' \ + | sed -E 's/\b[Pp]resently,? *//g' \ + | sed -E 's/\b[Aa]s of this writing,? *//g' \ + | sed -E 's/ +/ /g') + ;; + Netwrix.Plurals) + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/(\w+)\(s\)/\1s/g') + ;; + *) + continue + ;; + esac + + if [ "$NEW_CONTENT" != "$LINE_CONTENT" ]; then + awk -v n="$LINE_NUM" -v new="$NEW_CONTENT" 'NR==n{print new;next}1' "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE" + FIXED=$((FIXED + 1)) + fi + done + + if [ "$FIXED" -gt 0 ]; then + KEY="$RULE" + FIX_COUNTS[$KEY]=$(( ${FIX_COUNTS[$KEY]:-0} + FIXED )) + TOTAL_FIXES=$((TOTAL_FIXES + FIXED)) + fi + done + unset CODE_BLOCK_LINES +done + +# Map rules to human-readable categories for the summary comment +declare -A CATEGORY_MAP=( + ["Netwrix.Checkbox"]="Substitutions" + ["Netwrix.ClickOn"]="Substitutions" + ["Netwrix.Dropdown"]="Substitutions" + ["Netwrix.InOrderTo"]="Substitutions" + ["Netwrix.IsAbleTo"]="Substitutions" + ["Netwrix.LoginVerb"]="Substitutions" + ["Netwrix.MakeSure"]="Substitutions" + ["Netwrix.ProvidesAbilityTo"]="Substitutions" + ["Netwrix.SetupUsage"]="Substitutions" + ["Netwrix.Utilize"]="Substitutions" + ["Netwrix.WishTo"]="Substitutions" + ["Netwrix.Contractions"]="Contractions" + ["Netwrix.Please"]="Removed filler" + ["Netwrix.TemporalHedges"]="Removed filler" + ["Netwrix.Aforementioned"]="Removed filler" + ["Netwrix.LatinAbbreviations"]="Latin abbreviations" + ["Netwrix.Spacing"]="Spacing" + ["Netwrix.Plurals"]="Plurals" +) + +# Aggregate by category +declare -A CATEGORY_COUNTS +for KEY in "${!FIX_COUNTS[@]}"; do + CAT="${CATEGORY_MAP[$KEY]:-Other}" + CATEGORY_COUNTS[$CAT]=$(( ${CATEGORY_COUNTS[$CAT]:-0} + FIX_COUNTS[$KEY] )) +done + +# Output JSON summary grouped by category +echo "{" +echo " \"total\": $TOTAL_FIXES," +echo " \"by_category\": {" +FIRST=true +for CAT in "${!CATEGORY_COUNTS[@]}"; do + if [ "$FIRST" = true ]; then + FIRST=false + else + echo "," + fi + printf ' "%s": %d' "$CAT" "${CATEGORY_COUNTS[$CAT]}" +done +echo "" +echo " }" +echo "}" From c07d30e27f46577890facfaca06aa86fc159e698 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:42:50 -0500 Subject: [PATCH 06/14] fix: address code review issues in vale-autofix.sh - Use temp file for awk line replacement to prevent backslash corruption - Fix Please rule: skip please-note lines, remove all other please - Add word boundaries to Checkbox pattern - Fix stale CODE_BLOCK_LINES on skipped files - Use mapfile for safe file path iteration Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/vale-autofix.sh | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/scripts/vale-autofix.sh b/scripts/vale-autofix.sh index afc3461631..0827a867e7 100755 --- a/scripts/vale-autofix.sh +++ b/scripts/vale-autofix.sh @@ -16,15 +16,14 @@ TOTAL_FIXES=0 declare -A FIX_COUNTS # Get unique files from violations -FILES=$(jq -r '[.[].path] | unique | .[]' "$VIOLATIONS_FILE") +mapfile -t FILES_ARRAY < <(jq -r '[.[].path] | unique | .[]' "$VIOLATIONS_FILE") -for FILE in $FILES; do +for FILE in "${FILES_ARRAY[@]}"; do + unset CODE_BLOCK_LINES + declare -A CODE_BLOCK_LINES if [ ! -f "$FILE" ]; then continue fi - - # Build a set of line numbers inside fenced code blocks (``` or ~~~) - declare -A CODE_BLOCK_LINES IN_FENCE=0 LINENUM=0 while IFS= read -r fline || [ -n "$fline" ]; do @@ -61,7 +60,7 @@ for FILE in $FILES; do case "$RULE" in Netwrix.Checkbox) - NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/[Cc]heck [Bb]ox/checkbox/g') + NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Cc]heck [Bb]ox\b/checkbox/g') ;; Netwrix.ClickOn) NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b([Dd]ouble-[Cc]lick|[Rr]ight-[Cc]lick|[Ll]eft-[Cc]lick|[Ll]eft [Cc]lick|[Cc]lick) [Oo]n\b/\1/g') @@ -140,7 +139,12 @@ for FILE in $FILES; do | sed -E 's/\betc\./and so on/g') ;; Netwrix.Please) - NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/\b[Pp]lease ([^n])/\1/g; s/\b[Pp]lease$//g' | sed -E 's/ +/ /g') + # Skip lines containing "please note" (case-insensitive) — handled by NoteThat rule + if ! echo "$LINE_CONTENT" | grep -qiE '\bplease\s+note\b'; then + NEW_CONTENT=$(echo "$LINE_CONTENT" \ + | sed -E 's/\b[Pp]lease +//g' \ + | sed -E 's/ +/ /g') + fi ;; Netwrix.Spacing) NEW_CONTENT=$(echo "$LINE_CONTENT" | sed -E 's/([.!?]) +/\1 /g') @@ -161,7 +165,12 @@ for FILE in $FILES; do esac if [ "$NEW_CONTENT" != "$LINE_CONTENT" ]; then - awk -v n="$LINE_NUM" -v new="$NEW_CONTENT" 'NR==n{print new;next}1' "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE" + TMPNEW=$(mktemp) + printf '%s' "$NEW_CONTENT" > "$TMPNEW" + awk -v n="$LINE_NUM" -v newfile="$TMPNEW" \ + 'NR==n{while((getline line < newfile)>0) print line; close(newfile); next}1' \ + "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE" + rm -f "$TMPNEW" FIXED=$((FIXED + 1)) fi done @@ -172,7 +181,6 @@ for FILE in $FILES; do TOTAL_FIXES=$((TOTAL_FIXES + FIXED)) fi done - unset CODE_BLOCK_LINES done # Map rules to human-readable categories for the summary comment From adf8c4a45408c4d77edeb9ec05503427b19a778b Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:45:09 -0500 Subject: [PATCH 07/14] feat: add Vale auto-fix workflow Two-phase auto-fix: script fixes for mechanical rules, then Claude for AI-judgment rules. Posts summary comment on PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/vale-autofix.yml | 268 +++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 .github/workflows/vale-autofix.yml diff --git a/.github/workflows/vale-autofix.yml b/.github/workflows/vale-autofix.yml new file mode 100644 index 0000000000..b20534ade1 --- /dev/null +++ b/.github/workflows/vale-autofix.yml @@ -0,0 +1,268 @@ +name: Vale Auto-Fix + +on: + pull_request: + types: [opened, synchronize] + branches: + - dev + paths: + - 'docs/**/*.md' + - '!docs/**/CLAUDE.md' + - '!docs/**/SKILL.md' + - '!docs/kb/**' + +concurrency: + group: vale-autofix-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + vale-autofix: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.PAT_TOKEN }} + fetch-depth: 0 + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Get changed markdown files + id: changed-files + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + CHANGED_MD_FILES=$(gh pr diff "$PR_NUMBER" --name-only | grep -E '^docs/.*\.md$' | grep -v '/CLAUDE\.md$' | grep -v '/SKILL\.md$' | grep -v '^docs/kb/' || true) + if [ -z "$CHANGED_MD_FILES" ]; then + echo "No docs markdown files changed" + echo "count=0" >> "$GITHUB_OUTPUT" + else + echo "Changed markdown files:" + echo "$CHANGED_MD_FILES" + echo "count=$(echo "$CHANGED_MD_FILES" | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT" + echo "$CHANGED_MD_FILES" > /tmp/changed-files.txt + fi + + - name: Install Vale + if: steps.changed-files.outputs.count > 0 + run: | + wget -q https://github.com/errata-ai/vale/releases/download/v3.9.5/vale_3.9.5_Linux_64-bit.tar.gz -O /tmp/vale.tar.gz + tar -xzf /tmp/vale.tar.gz -C /tmp + chmod +x /tmp/vale + /tmp/vale --version + + - name: Run Vale on changed files + id: vale-initial + if: steps.changed-files.outputs.count > 0 + run: | + jq -n '[]' > /tmp/violations.json + + while IFS= read -r file; do + if [ -f "$file" ]; then + RESULT=$(/tmp/vale --output JSON "$file" 2>/dev/null || true) + if [ -n "$RESULT" ] && [ "$RESULT" != "{}" ]; then + echo "$RESULT" | jq --arg f "$file" ' + [to_entries[] | .value[] | {path: $f, line: .Line, check: .Check, message: .Message}] + ' > /tmp/vale-file.json + jq -s '.[0] + .[1]' /tmp/violations.json /tmp/vale-file.json > /tmp/vale-tmp.json + mv /tmp/vale-tmp.json /tmp/violations.json + fi + fi + done < /tmp/changed-files.txt + + TOTAL=$(jq 'length' /tmp/violations.json) + echo "total=$TOTAL" >> "$GITHUB_OUTPUT" + echo "Vale found $TOTAL issue(s)" + + - name: Phase 1 — Script fixes + id: phase1 + if: steps.vale-initial.outputs.total > 0 + run: | + chmod +x scripts/vale-autofix.sh + SUMMARY=$(./scripts/vale-autofix.sh /tmp/violations.json) + echo "$SUMMARY" > /tmp/phase1-summary.json + PHASE1_TOTAL=$(echo "$SUMMARY" | jq '.total') + echo "fixed=$PHASE1_TOTAL" >> "$GITHUB_OUTPUT" + echo "Phase 1 fixed $PHASE1_TOTAL issue(s)" + + - name: Commit Phase 1 fixes + id: phase1-commit + if: steps.phase1.outputs.fixed > 0 + run: | + if git diff --quiet; then + echo "committed=false" >> "$GITHUB_OUTPUT" + else + git add -A docs/ + git commit -m "fix(vale): auto-fix substitutions and removals" + git push + echo "committed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Re-run Vale for remaining violations + id: vale-remaining + if: steps.vale-initial.outputs.total > 0 + run: | + jq -n '[]' > /tmp/remaining-violations.json + + while IFS= read -r file; do + if [ -f "$file" ]; then + RESULT=$(/tmp/vale --output JSON "$file" 2>/dev/null || true) + if [ -n "$RESULT" ] && [ "$RESULT" != "{}" ]; then + echo "$RESULT" | jq --arg f "$file" ' + [to_entries[] | .value[] | {path: $f, line: .Line, check: .Check, message: .Message}] + ' > /tmp/vale-file.json + jq -s '.[0] + .[1]' /tmp/remaining-violations.json /tmp/vale-file.json > /tmp/vale-tmp.json + mv /tmp/vale-tmp.json /tmp/remaining-violations.json + fi + fi + done < /tmp/changed-files.txt + + REMAINING=$(jq 'length' /tmp/remaining-violations.json) + echo "remaining=$REMAINING" >> "$GITHUB_OUTPUT" + echo "$REMAINING remaining violation(s) for Phase 2" + + - name: Phase 2 — Claude fixes + id: phase2 + if: steps.vale-remaining.outputs.remaining > 0 + continue-on-error: true + uses: anthropics/claude-code-action@v1 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + show_full_output: true + prompt: | + You are a documentation fixer. Your job is to fix Vale linting violations in markdown files. + + Read docs/CLAUDE.md for Netwrix writing standards before making any changes. + + Here are the remaining Vale violations after mechanical fixes were applied: + + ```json + $(cat /tmp/remaining-violations.json) + ``` + + For each violation: + 1. Read the file and understand the context around the flagged line + 2. Apply a fix that resolves the Vale rule while preserving the author's meaning + 3. If you are NOT confident in a fix (ambiguous context, multiple valid interpretations, fix would change meaning) SKIP it + + After fixing, write a JSON summary to /tmp/phase2-summary.json with this structure: + ```json + { + "fixed": [ + {"path": "file.md", "line": 1, "check": "Netwrix.RuleName", "action": "brief description of fix"} + ], + "skipped": [ + {"path": "file.md", "line": 1, "check": "Netwrix.RuleName", "reason": "why it was skipped"} + ] + } + ``` + + After writing the summary, stage and commit your changes: + ```bash + git add -A docs/ + git commit -m "fix(vale): auto-fix rewrites (AI-assisted)" + git push + ``` + + IMPORTANT: Write the summary JSON file BEFORE committing. Do not post any PR comments. + claude_args: '--allowedTools "Bash(git:*),Read,Write,Edit,Glob,Grep"' + + - name: Build and post summary comment + if: steps.vale-initial.outputs.total > 0 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + REPO=${{ github.repository }} + + # Collect Phase 1 stats + PHASE1_TOTAL=0 + PHASE1_BODY="" + if [ -f /tmp/phase1-summary.json ]; then + PHASE1_TOTAL=$(jq '.total' /tmp/phase1-summary.json) + # Build category rows from Phase 1 + PHASE1_BODY=$(jq -r ' + .by_category | to_entries | sort_by(.key) | .[] | + "| \(.key) | \(.value) |" + ' /tmp/phase1-summary.json) + fi + + # Collect Phase 2 stats + PHASE2_FIXED=0 + PHASE2_SKIPPED=0 + PHASE2_BODY="" + SKIPPED_BODY="" + if [ -f /tmp/phase2-summary.json ]; then + PHASE2_FIXED=$(jq '.fixed | length' /tmp/phase2-summary.json) + PHASE2_SKIPPED=$(jq '.skipped | length' /tmp/phase2-summary.json) + + if [ "$PHASE2_FIXED" -gt 0 ]; then + PHASE2_BODY=$(jq -r ' + [.fixed[] | .check] | group_by(.) | .[] | + "| \(.[0] | sub("Netwrix\\."; "")) (rewrite) | \(length) |" + ' /tmp/phase2-summary.json) + fi + + if [ "$PHASE2_SKIPPED" -gt 0 ]; then + SKIPPED_BODY=$(jq -r ' + .skipped[] | + "| `\(.path):\(.line)` — \(.check) | \(.reason) |" + ' /tmp/phase2-summary.json) + fi + fi + + TOTAL_FIXED=$((PHASE1_TOTAL + PHASE2_FIXED)) + TOTAL_SKIPPED=$PHASE2_SKIPPED + + # Count affected files + FILE_COUNT=$(wc -l < /tmp/changed-files.txt | tr -d ' ') + + # Build the summary comment + { + echo "## Vale Auto-Fix Summary" + echo "" + echo "**${TOTAL_FIXED} issues fixed, ${TOTAL_SKIPPED} skipped across ${FILE_COUNT} files**" + echo "" + + if [ "$TOTAL_FIXED" -gt 0 ]; then + echo "| Category | Fixes |" + echo "|----------|-------|" + if [ -n "$PHASE1_BODY" ]; then + echo "$PHASE1_BODY" + fi + if [ -n "$PHASE2_BODY" ]; then + echo "$PHASE2_BODY" + fi + echo "" + fi + + if [ "$TOTAL_SKIPPED" -gt 0 ]; then + echo "| Skipped (needs manual review) | Reason |" + echo "|-------------------------------|--------|" + echo "$SKIPPED_BODY" + echo "" + fi + + echo 'Ask `@claude` on this PR if you'\''d like an explanation of any fix.' + } > /tmp/vale-summary.md + + # Delete previous Vale Auto-Fix comments + COMMENT_IDS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | contains("## Vale Auto-Fix Summary"))) | .id] | .[]' 2>/dev/null || true) + for ID in $COMMENT_IDS; do + gh api "repos/${REPO}/issues/comments/${ID}" -X DELETE 2>/dev/null || true + done + + # Post new summary + gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file /tmp/vale-summary.md From 0b185c5c653c3fabe560e466db1c5188c3a3dc96 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 16:45:51 -0500 Subject: [PATCH 08/14] remove: delete old Vale linter workflow and pre-push hook Replaced by vale-autofix.yml which auto-fixes issues instead of reporting them. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/vale-linter.yml | 224 ------------------------------ .husky/pre-push | 87 ------------ 2 files changed, 311 deletions(-) delete mode 100644 .github/workflows/vale-linter.yml delete mode 100755 .husky/pre-push diff --git a/.github/workflows/vale-linter.yml b/.github/workflows/vale-linter.yml deleted file mode 100644 index 89e0c3fd64..0000000000 --- a/.github/workflows/vale-linter.yml +++ /dev/null @@ -1,224 +0,0 @@ -name: Vale Linter - -on: - pull_request: - types: [opened, synchronize] - branches: - - dev - paths: - - 'docs/**/*.md' - - '!docs/**/CLAUDE.md' - - '!docs/**/SKILL.md' - - '!docs/kb/**' - -jobs: - vale-lint: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 1 - - - name: Get changed markdown files - id: changed-files - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUMBER=${{ github.event.pull_request.number }} - CHANGED_MD_FILES=$(gh pr diff "$PR_NUMBER" --name-only | grep -E '^docs/.*\.md$' | grep -v '/CLAUDE\.md$' | grep -v '/SKILL\.md$' | grep -v '^docs/kb/' || true) - if [ -z "$CHANGED_MD_FILES" ]; then - echo "No docs markdown files changed" - echo "count=0" >> "$GITHUB_OUTPUT" - else - echo "Changed markdown files:" - echo "$CHANGED_MD_FILES" - echo "count=$(echo "$CHANGED_MD_FILES" | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT" - echo "$CHANGED_MD_FILES" > /tmp/changed-files.txt - fi - - - name: Install Vale - if: steps.changed-files.outputs.count > 0 - run: | - wget -q https://github.com/errata-ai/vale/releases/download/v3.9.5/vale_3.9.5_Linux_64-bit.tar.gz -O /tmp/vale.tar.gz - tar -xzf /tmp/vale.tar.gz -C /tmp - chmod +x /tmp/vale - /tmp/vale --version - - - name: Run Vale on changed files - id: vale - if: steps.changed-files.outputs.count > 0 - run: | - # Collect all Vale violations into a single JSON array - jq -n '[]' > /tmp/vale-all.json - - while IFS= read -r file; do - if [ -f "$file" ]; then - RESULT=$(/tmp/vale --output JSON "$file" 2>/dev/null || true) - if [ -n "$RESULT" ] && [ "$RESULT" != "{}" ]; then - echo "$RESULT" | jq --arg f "$file" ' - [to_entries[] | .value[] | {path: $f, line: .Line, check: .Check, message: .Message}] - ' > /tmp/vale-file.json - jq -s '.[0] + .[1]' /tmp/vale-all.json /tmp/vale-file.json > /tmp/vale-tmp.json - mv /tmp/vale-tmp.json /tmp/vale-all.json - fi - fi - done < /tmp/changed-files.txt - - TOTAL=$(jq 'length' /tmp/vale-all.json) - echo "total_issues=$TOTAL" >> "$GITHUB_OUTPUT" - echo "Vale found $TOTAL issue(s)" - - - name: Parse diff for inline comment eligibility - id: diff-lines - if: steps.vale.outputs.total_issues > 0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUMBER=${{ github.event.pull_request.number }} - gh pr diff "$PR_NUMBER" > /tmp/pr-diff.txt - - # Extract lines visible in the diff per file (file:line format) - awk ' - /^\+\+\+ b\// { file = substr($0, 7) } - /^@@ / { - for (i = 1; i <= NF; i++) { - if ($i ~ /^\+[0-9]/) { - sub(/^\+/, "", $i) - split($i, range, ",") - start = range[1] + 0 - count = (range[2] != "" ? range[2] + 0 : 1) - for (j = start; j < start + count; j++) { - print file ":" j - } - break - } - } - } - ' /tmp/pr-diff.txt | sort -u > /tmp/diff-lines.txt - - echo "Diff lines extracted: $(wc -l < /tmp/diff-lines.txt)" - - - name: Build inline comments - id: inline - if: steps.vale.outputs.total_issues > 0 - run: | - # Filter violations to those on diff lines, take first 25 - jq -c '.[]' /tmp/vale-all.json | while read -r violation; do - KEY=$(echo "$violation" | jq -r '"\(.path):\(.line)"') - if grep -qxF "$KEY" /tmp/diff-lines.txt 2>/dev/null; then - echo "$violation" | jq '{ - path: .path, - line: .line, - side: "RIGHT", - body: ("**Vale** (`" + .check + "`): " + .message) - }' - fi - done | jq -s '.[0:25]' > /tmp/inline-comments.json - - INLINE_COUNT=$(jq 'length' /tmp/inline-comments.json) - echo "inline_count=$INLINE_COUNT" >> "$GITHUB_OUTPUT" - echo "Eligible inline comments: $INLINE_COUNT" - - - name: Build summary comment - if: steps.vale.outputs.total_issues > 0 - run: | - TOTAL=${{ steps.vale.outputs.total_issues }} - - # Build per-file tables - : > /tmp/vale-body.md - jq -r '[.[] | .path] | unique | .[]' /tmp/vale-all.json | while read -r filepath; do - { - echo "**${filepath}**" - echo "" - echo "| Line | Rule | Message |" - echo "|------|------|---------|" - jq -r --arg f "$filepath" ' - .[] | select(.path == $f) - | "| \(.line) | `\(.check)` | \(.message | gsub("\\|"; "\\\\|")) |" - ' /tmp/vale-all.json - echo "" - } >> /tmp/vale-body.md - done - - { - echo "## Vale Linting" - echo "" - echo "**Vale found ${TOTAL} issue(s)** across the changed files." - echo "" - cat /tmp/vale-body.md - echo "---" - echo "" - echo 'Fix these issues locally with `vale ` and push again, or comment `@claude` followed by your instructions (e.g., `@claude fix only the Vale issues`).' - echo "" - echo '> Automated fixes are only available for branches in this repository, not forks.' - } > /tmp/vale-comment.md - - - name: Delete previous Vale comments and reviews - if: steps.vale.outputs.total_issues > 0 || steps.changed-files.outputs.count > 0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUMBER=${{ github.event.pull_request.number }} - REPO=${{ github.repository }} - - # Resolve previous Vale inline comment threads - OWNER="${REPO%%/*}" - NAME="${REPO##*/}" - THREAD_IDS=$(gh api graphql -f query=' - query($owner:String!,$name:String!,$pr:Int!) { - repository(owner:$owner,name:$name) { - pullRequest(number:$pr) { - reviewThreads(first:100) { - nodes { id isResolved comments(first:1) { nodes { body } } } - } - } - } - }' -f owner="$OWNER" -f name="$NAME" -F pr="$PR_NUMBER" \ - --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].body | contains("**Vale**"))) | .id' 2>/dev/null || true) - for TID in $THREAD_IDS; do - gh api graphql -f query=' - mutation($tid:ID!) { - resolveReviewThread(input:{threadId:$tid}) { thread { isResolved } } - }' -f tid="$TID" 2>/dev/null || true - done - - # Delete previous Vale PR comments - COMMENT_IDS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | contains("## Vale Linting"))) | .id] | .[]' 2>/dev/null || true) - for ID in $COMMENT_IDS; do - gh api "repos/${REPO}/issues/comments/${ID}" -X DELETE 2>/dev/null || true - done - - # Dismiss previous Vale reviews - REVIEW_IDS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | contains("Vale found"))) | .id] | .[]' 2>/dev/null || true) - for ID in $REVIEW_IDS; do - gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews/${ID}/dismissals" -f message="Superseded by new review" -f event="DISMISS" 2>/dev/null || true - done - - - name: Post inline review - if: steps.inline.outputs.inline_count > 0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUMBER=${{ github.event.pull_request.number }} - REPO=${{ github.repository }} - TOTAL=${{ steps.vale.outputs.total_issues }} - - jq -n \ - --arg body "**Vale found ${TOTAL} issue(s).** See inline comments below. Full summary in the PR comment." \ - --argjson comments "$(cat /tmp/inline-comments.json)" \ - '{"body": $body, "event": "COMMENT", "comments": $comments}' \ - | gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" --input - 2>&1 - - - name: Post summary comment - if: steps.vale.outputs.total_issues > 0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUMBER=${{ github.event.pull_request.number }} - REPO=${{ github.repository }} - gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file /tmp/vale-comment.md diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 03d78832e3..0000000000 --- a/.husky/pre-push +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/sh - -# Vale pre-push hook -# Runs Vale on changed docs/*.md files before pushing. -# Currently in warning mode — shows errors but does not block the push. - -# Check if Vale is installed -if ! command -v vale >/dev/null 2>&1; then - echo "" - echo "WARNING: Vale is not installed." - echo "" - echo "Vale checks your writing against Netwrix style rules. Install it using one of these methods:" - echo "" - echo " macOS: brew install vale" - echo " Linux: sudo snap install vale" - echo " Windows: choco install vale" - echo " Manual: Download from https://github.com/errata-ai/vale/releases" - echo "" - echo "After installing, run 'vale --version' to verify." - echo "" - exit 0 -fi - -# Pre-push hook receives: as args -# and reads lines of: from stdin -ZERO_SHA="0000000000000000000000000000000000000000" - -CHANGED_MD_FILES="" -while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do - if [ "$LOCAL_SHA" = "$ZERO_SHA" ]; then - # Deleting a branch, nothing to check - continue - fi - - if [ "$REMOTE_SHA" = "$ZERO_SHA" ]; then - # New branch — compare against the merge base with dev - RANGE="$(git merge-base dev "$LOCAL_SHA" 2>/dev/null || echo "")..${LOCAL_SHA}" - if [ "$RANGE" = "..${LOCAL_SHA}" ]; then - # Fallback: check all files in the push - RANGE="$LOCAL_SHA" - fi - else - RANGE="${REMOTE_SHA}..${LOCAL_SHA}" - fi - - FILES=$(git diff --name-only --diff-filter=ACMR "$RANGE" 2>/dev/null | grep -E '^docs/.*\.md$' | grep -v '/CLAUDE\.md$' | grep -v '/SKILL\.md$' || true) - if [ -n "$FILES" ]; then - CHANGED_MD_FILES=$(printf "%s\n%s" "$CHANGED_MD_FILES" "$FILES" | sort -u | sed '/^$/d') - fi -done - -if [ -z "$CHANGED_MD_FILES" ]; then - exit 0 -fi - -echo "Running Vale on changed documentation files..." -echo "" - -VALE_FAILED=0 - -# Write file list to a temp file so we can iterate without a subshell -TMPFILE=$(mktemp) -echo "$CHANGED_MD_FILES" > "$TMPFILE" - -while IFS= read -r FILE; do - if [ -f "$FILE" ]; then - OUTPUT=$(vale "$FILE" 2>&1) - if echo "$OUTPUT" | grep -q "warning\|error"; then - echo "$OUTPUT" - echo "" - VALE_FAILED=1 - fi - fi -done < "$TMPFILE" - -rm -f "$TMPFILE" - -if [ "$VALE_FAILED" -eq 1 ]; then - echo "Vale found errors in the files above." - echo "Please fix these issues before or after pushing." - echo "" - # Warning mode: don't block the push - # TODO: Change to 'exit 1' once the team is comfortable with Vale - exit 0 -fi - -echo "Vale passed on all changed documentation files." From bdfa2144823864f6a8aa47bab3265f1ad27227ce Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 17:00:42 -0500 Subject: [PATCH 09/14] update: remove Vale fix logic from doc-pr-fix skill Vale issues are now auto-fixed by the vale-autofix workflow. This skill now handles only Dale and editorial fixes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/doc-pr-fix/SKILL.md | 40 +++++++----------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/.claude/skills/doc-pr-fix/SKILL.md b/.claude/skills/doc-pr-fix/SKILL.md index 7d17aae408..eef5b4a7a9 100644 --- a/.claude/skills/doc-pr-fix/SKILL.md +++ b/.claude/skills/doc-pr-fix/SKILL.md @@ -1,6 +1,6 @@ --- name: doc-pr-fix -description: "Autonomous fixer for documentation PRs. Triggered by @claude comments on PRs targeting dev. Reads the writer's request, the doc-pr review comment, and the Vale linting comment, then applies fixes and commits. Use this skill whenever a writer tags @claude on a documentation PR — not for interactive help (use doc-help for that), but for autonomous, single-shot fixes in CI." +description: "Autonomous fixer for documentation PRs. Triggered by @claude comments on PRs targeting dev. Reads the writer's request and the doc-pr review comment, then applies fixes and commits. Use this skill whenever a writer tags @claude on a documentation PR — not for interactive help (use doc-help for that), but for autonomous, single-shot fixes in CI." argument-hint: "[pr-number] [writer-comment]" --- @@ -20,8 +20,7 @@ You receive: Parse the writer's comment to determine what they want. Common patterns: -- **Fix all issues** — apply every fix from the doc-pr review comment and the Vale linting comment -- **Fix only Vale issues** — apply only fixes from the Vale linting comment +- **Fix all issues** — apply every fix from the doc-pr review comment (Dale + editorial) - **Fix only Dale issues** — apply only Dale linting fixes - **Fix a specific issue** — apply one targeted fix - **Improve flow/clarity/structure** — editorial rewrite of specific content @@ -37,25 +36,18 @@ Parse the writer's comment to determine what they want. Common patterns: gh api repos/{owner}/{repo}/issues/$PR_NUMBER/comments --jq '.[] | select(.body | contains("Documentation PR Review")) | .body' | tail -1 ``` This tells you what Dale and the editorial review flagged. -4. If the writer asks to fix Vale issues (or "all issues"), also find the Vale linting comment: - ```bash - gh api repos/{owner}/{repo}/issues/$PR_NUMBER/comments --jq '.[] | select(.user.login == "github-actions[bot]" and (.body | contains("## Vale Linting"))) | .body' | tail -1 - ``` - This gives you the Vale results table with file paths, line numbers, and rule violations. ## Step 3: Plan your work and post a progress comment Use Todo to create a task for each discrete piece of work you need to do. Build the task list from what you learned in Steps 1–2. Each task should be concrete and trackable. Mark each task as complete as you finish it. Example tasks for a "fix all issues" request: -- Fix Vale issues in `path/to/file.md` (N issues) -- Fix Vale issues in `path/to/other.md` (N issues) - Fix Dale issues in `path/to/file.md` (N issues) - Apply editorial suggestions - Verify changes - Commit and push -Only include tasks for what the writer actually asked for. If they said "fix only the Dale issues," your task list should contain Dale fixes, verify, and commit — no Vale tasks, no editorial tasks. The task list must reflect the writer's request exactly. +Only include tasks for what the writer actually asked for. If they said "fix only the Dale issues," your task list should contain Dale fixes, verify, and commit — no editorial tasks. The task list must reflect the writer's request exactly. Then post a PR comment mirroring your task list so the writer can see what you're doing: @@ -63,8 +55,8 @@ Then post a PR comment mirroring your task list so the writer can see what you'r PROGRESS_COMMENT_ID=$(gh pr comment "$PR_NUMBER" --body "$(cat <<'EOF' **Fix in progress:** -- [ ] Fix Vale issues in `path/to/file.md` (N issues) -- [ ] Fix Vale issues in `path/to/other.md` (N issues) +- [ ] Fix Dale issues in `path/to/file.md` (N issues) +- [ ] Apply editorial suggestions - [ ] Verify changes - [ ] Commit and push EOF @@ -84,7 +76,6 @@ Update the PR comment at natural milestones (after finishing each file, after co Work through the requested fixes methodically: -- For **Vale fixes**: read `docs/CLAUDE.md` for Vale guidance (especially the two rules requiring extra care), then fix each flagged issue in order, file by file - For **Dale fixes**: fix each flagged issue in order, file by file - For **editorial fixes from the review**: apply the suggested changes from the review comment - For **broader editorial requests** ("improve the flow", "make this clearer", "help with structure"): invoke `/doc-help` with the file path and the writer's request. Doc-help will analyze the document using its structured editing framework (structure, clarity, voice, surface). Since this is running in CI without an interactive writer, apply all of doc-help's suggestions autonomously rather than waiting for feedback @@ -93,20 +84,7 @@ Work through the requested fixes methodically: When editing: - Use the Edit tool for targeted changes, Write for larger rewrites - Preserve the author's meaning and intent — fix the style, don't rewrite the content -- Only change what was requested; don't fix other categories of issues even if they're on the same line (e.g., if asked to fix Vale issues, don't also fix Dale or editorial issues) - -## Step 4b: Update the Vale Idioms rule - -After applying fixes, check whether any Dale `idioms` violations or editorial items tagged `[idiom]` contained phrases not already in `.vale/styles/Netwrix/Idioms.yml`. For each new idiom: - -1. Read `.vale/styles/Netwrix/Idioms.yml` to confirm the phrase isn't already covered (check for both exact matches and regex patterns that would match it). -2. Add a new token entry under the most appropriate category comment. Follow existing conventions: - - Use `\b` word boundaries for multi-word phrases. - - Add optional inflection suffixes where the idiom can be conjugated (e.g., `\bgets? the ball rolling\b`). - - Use single quotes around each token. -3. Include the Idioms.yml file in your commit so the Vale rule grows over time. - -If no new idioms were found, skip this step. +- Only change what was requested; don't fix other categories of issues even if they're on the same line (e.g., if asked to fix Dale issues, don't also fix editorial issues) ## Step 5: Verify @@ -135,8 +113,8 @@ gh api repos/{owner}/{repo}/issues/comments/$PROGRESS_COMMENT_ID \ -X PATCH -f body="$(cat <<'EOF' **Fix complete:** -- [x] Fix Vale issues in `path/to/file.md` (N issues) -- [x] Fix Vale issues in `path/to/other.md` (N issues) +- [x] Fix Dale issues in `path/to/file.md` (N issues) +- [x] Apply editorial suggestions - [x] Verify changes - [x] Commit and push @@ -152,7 +130,7 @@ Skip progress tracking only for pure explanations (e.g., "why is this flagged?") ## Behavioral Notes - **Fix what's clear, ask about what isn't.** If a request has both obvious parts and ambiguous parts, apply the obvious fixes, commit and push those, then post a comment that summarizes what you did AND asks clarifying questions about the rest. The writer can reply with another `@claude` comment to continue. -- **Never fix issues the writer didn't ask about.** If they said "fix the Vale issues," only fix Vale issues — don't also fix Dale issues, editorial issues, or rewrite sentences for clarity, even if the fix is on the same line. +- **Never fix issues the writer didn't ask about.** If they said "fix the Dale issues," only fix Dale issues — don't also fix editorial issues or rewrite sentences for clarity, even if the fix is on the same line. - **If a fix would substantially change the author's meaning**, skip it and explain why in your summary comment. Ask the writer how they'd like to handle it. - **If the entire request is unclear**, don't edit anything — post a comment asking for clarification. It's better to ask one good question than to guess wrong and push unwanted changes. - **Each `@claude` comment is a fresh invocation.** You won't remember previous runs, so always re-read the PR diff and review comment for context. From 032c71a7c50bd45869889e314d544d0818f34892 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 17:01:44 -0500 Subject: [PATCH 10/14] update: remove Vale inline comment references from doc-pr skill Vale issues are now auto-fixed by the vale-autofix workflow. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/doc-pr/SKILL.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.claude/skills/doc-pr/SKILL.md b/.claude/skills/doc-pr/SKILL.md index c4b85103b3..c809b7cf9e 100644 --- a/.claude/skills/doc-pr/SKILL.md +++ b/.claude/skills/doc-pr/SKILL.md @@ -1,6 +1,6 @@ --- name: doc-pr -description: "Orchestrate a documentation review for pull requests targeting dev. Runs Dale linting and editorial review on changed markdown files, then posts a structured comment to the PR. Vale linting runs separately via the vale-linter workflow (inline review comments + summary PR comment). Use this skill whenever a PR involves markdown files in docs/ and targets the dev branch — triggered automatically by the doc-pr GitHub Actions workflow on PR open, sync, or when invoked manually via /doc-pr." +description: "Orchestrate a documentation review for pull requests targeting dev. Runs Dale linting and editorial review on changed markdown files, then posts a structured comment to the PR. Vale issues are auto-fixed separately by the vale-autofix workflow. Use this skill whenever a PR involves markdown files in docs/ and targets the dev branch — triggered automatically by the doc-pr GitHub Actions workflow on PR open, sync, or when invoked manually via /doc-pr." argument-hint: "[changed-files-csv] [pr-number]" --- @@ -8,7 +8,7 @@ argument-hint: "[changed-files-csv] [pr-number]" You orchestrate a two-stage documentation review pipeline for pull requests. Your job is to run each stage, collect the results, and post a single comprehensive review comment to the PR. -Vale linting runs separately (via the vale-linter workflow) and posts inline review comments plus a summary PR comment. Do not run Vale or include Vale results in your review. +Vale issues are auto-fixed separately by the vale-autofix workflow. Do not run Vale or include Vale results in your review. Read `docs/CLAUDE.md` before starting — it contains the writing standards and Vale guidance you need for the editorial review stage. @@ -99,7 +99,7 @@ Write the full review body to `/tmp/doc-pr-review.md` using the Write tool. Foll ### Summary -N Dale issues, N editorial suggestions across N files. Vale issues are posted in a separate comment by the vale-linter workflow. +N Dale issues, N editorial suggestions across N files. Vale issues are auto-fixed separately. --- @@ -107,8 +107,7 @@ N Dale issues, N editorial suggestions across N files. Vale issues are posted in Comment `@claude` on this PR followed by your instructions to get help: -- `@claude fix all issues` — fix all Vale, Dale, and editorial issues -- `@claude fix only the Vale issues` — fix just the Vale issues +- `@claude fix all issues` — fix all Dale and editorial issues - `@claude help improve the flow of this document` — get writing assistance - `@claude explain the voice issues` — understand why something was flagged From 867eb8bf3ec8746ae8bcaa9af87e583278e7ac0b Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 17:02:43 -0500 Subject: [PATCH 11/14] update: remove Vale references from doc-pr workflow Vale is now handled by vale-autofix workflow. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/claude-doc-pr.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/claude-doc-pr.yml b/.github/workflows/claude-doc-pr.yml index 7708e0ead0..768609dffa 100644 --- a/.github/workflows/claude-doc-pr.yml +++ b/.github/workflows/claude-doc-pr.yml @@ -168,7 +168,7 @@ jobs: - Dale issues: ${{ steps.dale-post.outputs.dale_count }} (already posted as inline comments) - PR diff is at: /tmp/pr-diff.txt - NOTE: Vale linting runs separately (vale-linter workflow) and posts inline review comments plus a summary PR comment. Do not run Vale or include Vale issues in this review. + NOTE: Vale issues are auto-fixed separately by the vale-autofix workflow. Do not run Vale or include Vale issues in this review. INSTRUCTIONS: @@ -289,7 +289,7 @@ jobs: OWNER="${REPO%%/*}" NAME="${REPO##*/}" - # Resolve all Dale and Vale inline comment threads + # Resolve all Dale inline comment threads THREAD_IDS=$(gh api graphql -f query=' query($owner:String!,$name:String!,$pr:Int!) { repository(owner:$owner,name:$name) { @@ -300,7 +300,7 @@ jobs: } } }' -f owner="$OWNER" -f name="$NAME" -F pr="$PR_NUMBER" \ - --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and ((.comments.nodes[0].body | contains("**Dale**")) or (.comments.nodes[0].body | contains("**Vale**")))) | .id' 2>/dev/null || true) + --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and (.comments.nodes[0].body | contains("**Dale**"))) | .id' 2>/dev/null || true) for TID in $THREAD_IDS; do gh api graphql -f query=' mutation($tid:ID!) { @@ -308,9 +308,9 @@ jobs: }' -f tid="$TID" 2>/dev/null || true done - # Dismiss all previous Dale and Vale reviews + # Dismiss all previous Dale reviews REVIEW_IDS=$(gh api repos/${REPO}/pulls/${PR_NUMBER}/reviews \ - --jq '[.[] | select(.user.login == "github-actions[bot]" and ((.body | contains("Dale found")) or (.body | contains("Vale found")))) | .id] | .[]' 2>/dev/null || true) + --jq '[.[] | select(.user.login == "github-actions[bot]" and (.body | contains("Dale found"))) | .id] | .[]' 2>/dev/null || true) for ID in $REVIEW_IDS; do gh api repos/${REPO}/pulls/${PR_NUMBER}/reviews/${ID}/dismissals \ -f message="Superseded after fixes applied" -f event="DISMISS" 2>/dev/null || true From 8ed9a6ac687b834f5b3a416ca8a874209658c88b Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 17:04:28 -0500 Subject: [PATCH 12/14] docs: update all references to reflect Vale auto-fix flow Vale issues are now auto-fixed on PRs. Pre-push hook removed. Inline comments removed. Updated CLAUDE.md, docs/CLAUDE.md, CONTRIBUTING.md, and README.md. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- CONTRIBUTING.md | 16 +++++++--------- README.md | 9 ++++----- docs/CLAUDE.md | 9 +++------ 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e20a9d371b..0fa8309d35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,7 +69,7 @@ PRs target `dev`. Never commit directly to `dev` or `main`. The `sync-dev-to-mai | Workflow | Trigger | Purpose | |---|---|---| | `build-and-deploy.yml` | Push to main/dev, PRs to dev | Build and deploy to Azure | -| `vale-linter.yml` | PRs with `.md` changes | Vale inline review comments (up to 25) + summary PR comment | +| `vale-autofix.yml` | PRs with `.md` changes | Auto-fix Vale issues (script + AI), post summary comment | | `claude-doc-pr.yml` | PRs to dev with `docs/` changes | Dale + editorial review; `@claude` follow-up | | `claude-documentation-reviewer.yml` | PRs with `.md` changes | AI review with inline suggestions | | `claude-documentation-fixer.yml` | `@claude` comment on PR | Apply fixes and push | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f1cafc224..3ed01f0a63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,11 +7,11 @@ Thank you for contributing to Netwrix product documentation. This guide covers e - **Node.js 22+** - **npm** - **Git** -- **Vale** (style linter — required for pushing documentation changes) +- **Vale** (style linter — optional for local use; issues are auto-fixed on PRs) ### Install Vale -[Vale](https://vale.sh/) is a command-line linter for prose. It checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale is required to push changes to documentation files. The pre-push hook runs Vale automatically and blocks pushes that have linting errors. +[Vale](https://vale.sh/) is a command-line linter for prose. It checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale issues are auto-fixed on PRs, but you can install it locally to preview issues before pushing. **macOS:** ```bash @@ -57,12 +57,12 @@ The dev server runs on port 4500 with hot reload — changes you make to documen 1. Create a branch from `dev` (never commit directly to `dev` or `main`). 2. Make your changes to documentation files in `docs/`. -3. Run Vale on your changed files to catch linting errors. +3. Optionally run Vale on your changed files to preview issues (they'll be auto-fixed on the PR). 4. Test the build with `npm run build`. -5. Push your branch (the pre-push hook runs Vale automatically and warns about any errors). +5. Push your branch. 6. Create a pull request (PR) targeting `dev`. -After you open a PR, an automated review runs Vale linting, Dale linting (AI-powered), and an editorial review, then posts the results as PR comments. If you want help applying the suggested fixes, comment `@claude` on the PR followed by your request and Claude will apply fixes and push a commit. If Claude needs clarification, it will ask in the PR comments. +After you open a PR, Vale issues are auto-fixed and a summary is posted. Dale linting (AI-powered) and an editorial review also run and post results as PR comments. To get help with Dale or editorial suggestions, comment `@claude` on the PR followed by your request. ## Writing standards @@ -92,9 +92,7 @@ description: 'SEO description' ## Linting with Vale -Vale enforces 30 Netwrix-specific rules covering word choice, punctuation, formatting, and common writing issues. The pre-push hook runs Vale automatically and warns about errors in changed `docs/*.md` files. - -Run Vale on a file before pushing to catch issues early: +Vale enforces 30 Netwrix-specific rules covering word choice, punctuation, formatting, and common writing issues. Vale issues are auto-fixed on PRs, but you can run it locally to preview: ```bash vale docs/path/to/file.md @@ -187,7 +185,7 @@ For tasks that need design decisions first — like writing a new guide from scr ## Common Mistakes - Don't manually copy KB content into versioned product folders — it's managed by the KB script. -- Don't ignore Vale warnings before pushing — fix errors to keep documentation clean. +- Vale issues are auto-fixed on PRs, but running Vale locally helps catch issues early. - Don't commit directly to `dev` or `main` — create a branch from `dev` first. - Don't target `main` in PRs — always use `dev`. - Don't use first person anywhere in documentation content. diff --git a/README.md b/README.md index 516bf05e52..71804482aa 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ This documentation site serves all Netwrix product documentation. - **Node.js 22+** - **npm** - **Git** -- **Vale** (style linter — required for pushing documentation changes) +- **Vale** (style linter — optional for local use; issues are auto-fixed on PRs) ### Install Vale -[Vale](https://vale.sh/) is a command-line linter for prose. A linter checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale is required to push changes to documentation files. The pre-push hook runs Vale automatically and blocks pushes that have linting errors. +[Vale](https://vale.sh/) is a command-line linter for prose. A linter checks your writing against a set of style rules — like a spell checker, but for grammar, word choice, and tone. Vale issues are auto-fixed on PRs, but you can install it locally to preview issues before pushing. **macOS:** ```bash @@ -74,7 +74,7 @@ npm run start ### Run Vale Locally -Run Vale on a file before pushing to catch issues early: +Vale issues are auto-fixed on PRs, but you can run Vale locally to preview: ```bash vale docs/path/to/file.md @@ -97,8 +97,7 @@ git diff --name-only dev | grep '^docs/.*\.md$' | xargs vale │ │ ├── doc-pr-fix/ # Autonomous PR fixer (@claude) │ │ └── doc-help/ # Interactive writing assistant │ └── agents/ # Autonomous worker agents -├── .husky/ -│ └── pre-push # Vale pre-push hook (blocks on lint errors) +├── .husky/ # Git hooks (managed by Husky) ├── .vale/ │ └── styles/ │ └── Netwrix/ # 30 Vale linting rules (YAML) diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 1b305e1c17..4a3d1cdff1 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -62,9 +62,9 @@ The four core qualities: ## Linting -### Vale (pre-push) +### Vale -Vale runs via a pre-push hook that checks changed `docs/*.md` files and reports errors. The hook currently runs in warning mode — it shows errors but does not block the push. Always run Vale locally and fix all errors before pushing. +Vale enforces 30 Netwrix-specific rules in `.vale/styles/Netwrix/` covering word choice, punctuation, formatting, and common writing issues. Vale issues are auto-fixed on PRs by the `vale-autofix` workflow — you don't need to fix them manually before pushing. You can still run Vale locally to preview issues: ```bash vale @@ -124,9 +124,7 @@ Each step is a single action. Lead with the UI element or command: ## CI/CD Context -**Vale (pre-push hook)** — Runs automatically before every push. Currently in warning mode — shows errors but does not block the push. Writers should fix all Vale errors locally before pushing. - -**Vale (PR review)** — Runs on PRs to `dev` with docs changes. Posts up to 25 inline review comments on changed lines plus a summary PR comment. +**Vale (auto-fix)** — Runs on PRs to `dev` with docs changes. Automatically fixes Vale issues in two phases (script for mechanical rules, Claude for judgment-based rules) and posts a summary comment. No inline comments. **Doc PR review** — Runs on PRs to `dev` with docs changes. Posts Dale inline comments and an editorial review summary. Does not block merges. @@ -139,7 +137,6 @@ Each step is a single action. Lead with the UI element or command: ## Common Mistakes - Don't manually copy KB content into versioned product folders — it's managed by the KB script -- Don't ignore Vale warnings before pushing — fix errors to keep documentation clean - Don't commit directly to `dev` or `main` — create a branch from `dev` first - Don't target `main` in PRs — use `dev` - Don't use first person anywhere in documentation content From 3f9cabef0e6d11f863d282d837e65bd5e2a61cab Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 17:04:57 -0500 Subject: [PATCH 13/14] fix: remove duplicate Vale paragraph in docs/CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 4a3d1cdff1..d043ae7b30 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -70,7 +70,7 @@ Vale enforces 30 Netwrix-specific rules in `.vale/styles/Netwrix/` covering word vale ``` -Vale enforces 30 Netwrix-specific rules in `.vale/styles/Netwrix/` covering word choice, punctuation, formatting, and common writing issues. Two rules require extra care: +Two rules require extra care: - **`NoteThat`** — Replace "Note that..." or "Please note..." with an admonition block: ```md From 27a3cb036162e04b6524e46c8c9f6282508c61b7 Mon Sep 17 00:00:00 2001 From: jth-nw Date: Thu, 19 Mar 2026 17:05:55 -0500 Subject: [PATCH 14/14] fix: remove stale pre-push hook references from install comments Co-Authored-By: Claude Opus 4.6 (1M context) --- CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ed01f0a63..2e2a46c089 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ vale --version git clone https://github.com/netwrix/docs.git cd docs -# Install dependencies (also sets up the pre-push hook via Husky) +# Install dependencies npm install # Start development server diff --git a/README.md b/README.md index 71804482aa..c0ca3a3f31 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ vale --version git clone https://github.com/netwrix/docs.git cd docs -# Install dependencies (also sets up the pre-push hook via Husky) +# Install dependencies npm install # Start development server