Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,33 @@ on:
pull_request:

jobs:
test:
python:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: actions/setup-node@v4
with:
node-version: '20'
python-version: ${{ matrix.python-version }}

- name: linter selftest
run: python3 tools/lint.py --selftest

- name: hook selftest
run: node hooks/selftest.js

- name: ast selftest
run: python3 tools/ast_check.py --selftest

# The plugin's own Python has to survive its own structural rules.
- name: changelog extractor selftest
run: python3 tools/changelog_section.py --selftest

- name: install tree-sitter for the JS ast check
run: pip install tree-sitter tree-sitter-language-pack

# With tree-sitter present, ast_check parses the hooks JavaScript too.
- name: ast check own source
run: python3 tools/ast_check.py --check tools/
run: python3 tools/ast_check.py --check hooks/ tools/

# The deep-dive docs quote the banned list and carry an ignore-file marker,
# so the linter skips them. README, CONTRIBUTING, and CHANGELOG get checked.
- name: lint own docs
run: python3 tools/lint.py --check README.md docs/CONTRIBUTING.md docs/CHANGELOG.md docs/hooks.md docs/linting.md

Expand All @@ -44,3 +45,16 @@ jobs:

- name: lint own comments
run: python3 tools/lint.py --check --code tools/ hooks/ install.sh

node:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18', '20', '22']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: hook selftest
run: node hooks/selftest.js
39 changes: 39 additions & 0 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: anti-slop review

# Runs Claude on each PR to flag writing slop in the diff. Needs the repo secret
# ANTHROPIC_API_KEY (or run /install-github-app). Fork PRs get no secret, so the
# job is gated to same-repo branches.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

jobs:
review:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && secrets.ANTHROPIC_API_KEY != '' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Review the diff on this pull request against this repo's anti-slop
rules (hooks/rules/core.md and patterns.json). Check only the added
or changed lines. Flag:
- banned vocabulary (leverage, delve, crucial, robust, seamless, load-bearing, and the rest of patterns.json)
- contrast constructions ("it's not X, it's Y", "X isn't just Y")
- em-dashes in prose
- sycophancy and servile closers
- preamble or closing-summary padding in comments or the PR body
Post one concise summary comment with file:line for each hit. If the
diff is clean, say so in one line and stop.
claude_args: |
--model claude-sonnet-5
--max-turns 6
25 changes: 25 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: release

# Push a tag like v0.3.1 and this cuts the GitHub release from the matching
# docs/CHANGELOG.md section.
on:
push:
tags: ['v*']

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: build release notes
run: python3 tools/changelog_section.py "${GITHUB_REF_NAME}" > "${RUNNER_TEMP}/notes.md"
- name: create release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${GITHUB_REF_NAME}" --title "${GITHUB_REF_NAME}" --notes-file "${RUNNER_TEMP}/notes.md"
3 changes: 0 additions & 3 deletions hooks/selftest.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ assert.ok(
);
assert.ok(!hit('- one a\n- **Two** b\n- three c'), 'bold-bullet false positive');

// The servile closer.
assert.ok(hit("Say the word and I'll add the counter."), 'servile closer missed');
assert.ok(hit('Just let me know.'), 'servile closer missed');
assert.ok(hit('Happy to walk through the retry path.'), 'servile closer missed');
Expand Down Expand Up @@ -86,7 +85,6 @@ assert.ok(!hit('Postgres or SQLite?'), 'plain question blocked');
assert.ok(hard.some(v => !v.soft), 'appositive contrast must block');
}

// Preamble and narration.
assert.ok(hit('Yes, I read it. The lock is held.'), 'narration missed');
assert.ok(hit("Here's the actual shape of the deploy."), 'preamble missed');
assert.ok(hit('One thing I found while reading matters more.'), 'buried finding missed');
Expand All @@ -101,7 +99,6 @@ assert.ok(!hit('The runner polls /health until the version matches.'), 'plain re
assert.ok(!check('The registry holds one lock.').some(v => v.soft), 'short reply flagged');
}

// Ambiguous words never block.
assert.ok(!hit('The harness runs the hook.'), 'ambiguous word blocked');
assert.ok(!hit('A robust retry covers the landscape.'), 'ambiguous words blocked');

Expand Down
59 changes: 59 additions & 0 deletions tools/changelog_section.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Print one version's section from docs/CHANGELOG.md.

python3 tools/changelog_section.py 0.3.0
python3 tools/changelog_section.py v0.3.0 # a leading v is stripped

release.yml pipes the output into `gh release create --notes-file`.
"""

import sys
from pathlib import Path


def section(text: str, version: str) -> str:
"""Return the body under `## [version]`, up to the next `## [` header.

Link-reference lines (`[0.3.0]: https://...`) at the file's foot stay out.
"""
out, capture = [], False
header = f"## [{version}]"
for line in text.split("\n"):
if line.startswith("## ["):
if capture:
break
capture = line.strip().startswith(header)
continue
if capture and not line.startswith("["):
out.append(line)
return "\n".join(out).strip()


def selftest():
sample = (
"# Changelog\n\n"
"## [0.3.0]\n\n### Added\n- A thing.\n\n"
"## [0.2.0]\n\n### Added\n- An older thing.\n\n"
"[0.3.0]: https://example.com/v0.3.0\n"
)
assert section(sample, "0.3.0") == "### Added\n- A thing.", "0.3.0 body wrong"
assert section(sample, "0.2.0") == "### Added\n- An older thing.", "0.2.0 body wrong"
assert section(sample, "9.9.9") == "", "missing version must be empty"
print("changelog_section selftest ok")


def main():
if "--selftest" in sys.argv:
selftest()
return
version = sys.argv[1].lstrip("v")
path = Path(__file__).resolve().parent.parent / "docs" / "CHANGELOG.md"
body = section(path.read_text(), version)
if not body:
print(f"no changelog section for {version}", file=sys.stderr)
sys.exit(1)
print(body)


if __name__ == "__main__":
main()
Loading