diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 6cb0b00..8c30b65 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -88,4 +88,39 @@ for p in "${PREAMBLE_PATTERNS[@]}"; do fi done +# Rule 5.1: Cursor attribution. Reject Cursor co-author trailers, the agent +# email, any @cursor.com address, and "generated with Cursor" style tells. +# Case-insensitive on purpose: cursoragent@cursor.com, Co-authored-by: Cursor. +CURSOR_LITERALS=( + 'Co-Authored-By: Cursor' + 'Co-authored-by: Cursor' + 'Generated with Cursor' + 'Generated by Cursor' + 'with help from Cursor' + 'Powered by Cursor' +) +for p in "${CURSOR_LITERALS[@]}"; do + if printf '%s' "$CONTENT" | grep -qiF -- "$p"; then + printf 'commit-msg: forbidden Cursor attribution: %s\n' "$p" >&2 + printf ' Rule: github-discipline.md Rule 5.1\n' >&2 + FAIL=1 + fi +done +if printf '%s' "$CONTENT" | grep -qiE -- '@cursor\.com'; then + printf 'commit-msg: forbidden Cursor email (@cursor.com) in commit message.\n' >&2 + printf ' Rule: github-discipline.md Rule 5.1\n' >&2 + FAIL=1 +fi + +# Author / committer identity must not be Cursor. git var resolves the exact +# identity (including --author / GIT_*_IDENT overrides) this commit will record. +for ident in "$(git var GIT_AUTHOR_IDENT 2>/dev/null)" "$(git var GIT_COMMITTER_IDENT 2>/dev/null)"; do + [ -z "$ident" ] && continue + if printf '%s' "$ident" | grep -qiE -- '@cursor\.com|(^|[^[:alpha:]])cursor(agent)?([^[:alpha:]]|$)'; then + printf 'commit-msg: forbidden Cursor identity as author/committer: %s\n' "$ident" >&2 + printf ' Fix: set git config user.name / user.email to your own identity.\n' >&2 + FAIL=1 + fi +done + exit $FAIL diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 3e9cc0a..603a28c 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -73,8 +73,12 @@ SKIP_PATTERNS=( # .githooks/pre-commit) rather than the canonical *-style-check names. '/commit-msg$' '/pre-commit$' + '/pre-push$' '/check-style\.sh$' '/check-staged-style\.sh$' + # The CI workflow that enforces the same Cursor prohibition server-side; it + # must quote the forbidden tokens to scan for them. + '^\.github/workflows/no-cursor-attribution\.yml$' ) # Filter STAGED_FILES through the skip list. @@ -147,6 +151,16 @@ LITERAL_PATTERNS=( 'This change was generated' ) +# Cursor-attribution literal tells (case-insensitive at the call site). +CURSOR_LITERALS=( + 'Co-Authored-By: Cursor' + 'Co-authored-by: Cursor' + 'Generated with Cursor' + 'Generated by Cursor' + 'with help from Cursor' + 'Powered by Cursor' +) + # AI-tell signature emojis (UTF-8 byte sequences kept in a variable so the # script source stays clean of the glyphs themselves). EMOJI_TELLS=$(printf '\xf0\x9f\xa4\x96\n\xe2\x9c\xa8\n\xf0\x9f\xaa\x84\n\xf0\x9f\xa7\xa0\n\xf0\x9f\xa6\xbe\n\xf0\x9f\xa4\x9d') @@ -180,6 +194,21 @@ while IFS= read -r file; do fi done + # Cursor-attribution check (mirrors commit-msg). Specific tells only, never + # the bare word "cursor" (a legitimate programming term: text/db cursor). + for p in "${CURSOR_LITERALS[@]}"; do + if printf '%s' "$ADDED" | grep -qiF -- "$p"; then + printf 'pre-commit: forbidden Cursor attribution in staged %s: %s\n' "$file" "$p" >&2 + printf ' Rule: github-discipline.md Rule 5.1\n' >&2 + FAIL=1 + fi + done + if printf '%s' "$ADDED" | grep -qiE -- '@cursor\.com'; then + printf 'pre-commit: forbidden Cursor email (@cursor.com) in staged %s.\n' "$file" >&2 + printf ' Rule: github-discipline.md Rule 5.1\n' >&2 + FAIL=1 + fi + # AI-signature emoji check. while IFS= read -r emoji; do [ -z "$emoji" ] && continue diff --git a/.github/workflows/no-cursor-attribution.yml b/.github/workflows/no-cursor-attribution.yml new file mode 100644 index 0000000..23cf811 --- /dev/null +++ b/.github/workflows/no-cursor-attribution.yml @@ -0,0 +1,60 @@ +name: No Cursor attribution + +# Server-side companion to the local .githooks/commit-msg and pre-commit guards. +# Fails the check if any commit pushed (or in a PR) carries Cursor attribution: +# a Cursor co-author trailer, an @cursor.com email, or a "generated with Cursor" +# tell, in the commit message or in the author/committer identity. + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + scan: + name: Scan commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Reject Cursor attribution + env: + EVENT: ${{ github.event_name }} + PR_BASE: ${{ github.event.pull_request.base.sha }} + PR_HEAD: ${{ github.event.pull_request.head.sha }} + PUSH_BEFORE: ${{ github.event.before }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + + # Resolve the set of commits to scan. + if [ "$EVENT" = "pull_request" ]; then + range="${PR_BASE}..${PR_HEAD}" + elif [ -n "${PUSH_BEFORE:-}" ] && [ "$PUSH_BEFORE" != "0000000000000000000000000000000000000000" ]; then + range="${PUSH_BEFORE}..${SHA}" + else + # New branch / tag push or manual run: scan just the tip commit. + range="${SHA}~1..${SHA}" + git rev-parse "${SHA}~1" >/dev/null 2>&1 || range="$SHA" + fi + echo "Scanning commit range: $range" + + # Emit message + author + committer for each commit, then grep. + fmt='commit %H%nauthor: %an <%ae>%ncommitter: %cn <%ce>%n%B' + if printf '%s' "$range" | grep -q '\.\.'; then + log=$(git log --format="$fmt" "$range") + else + log=$(git log -1 --format="$fmt" "$range") + fi + + pattern='cursoragent@cursor\.com|@cursor\.com|Co-authored-by:[[:space:]]*Cursor|Generated (with|by) Cursor|with help from Cursor|Powered by Cursor' + if printf '%s\n' "$log" | grep -niE -- "$pattern"; then + echo "::error::Cursor attribution found in the commits above. Strip it (rewrite the commits) before pushing." + exit 1 + fi + echo "No Cursor attribution found."