From 081392dcb6f66e9de54fe9f5c036133459081155 Mon Sep 17 00:00:00 2001 From: Jesse Koldewijn Date: Thu, 12 Nov 2026 00:39:20 +0100 Subject: [PATCH 1/7] ci: close attribution-gate gaps from Ox-Agent review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same three findings CodeRabbit filed on the Ox-Agent port: - pull_request: add 'edited' so post-green title/body edits re-fire commit-lint instead of evading the gate. - commit-check: flag standalone 'via|by|using|with ' lines — the generator-footer regex needed a leading verb. - commit-check: scope the AI co-author rule to machine signals ([bot], bot@, ai-integration, vendor domains) plus exact vendor display names; human co-authors whose names contain a vendor token ("Claude Martin") no longer false-positive. --- .github/workflows/ci.yml | 3 +++ scripts/commit-check.sh | 19 +++++++++++++++++-- src/pages/index.astro | 30 +++++++++++------------------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ecafc4..0ec2940 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,9 @@ name: ci on: pull_request: + # edited re-fires the gate on title/body changes — otherwise a + # post-green metadata edit could land attribution unchecked. + types: [opened, synchronize, reopened, edited] push: branches: [main] diff --git a/scripts/commit-check.sh b/scripts/commit-check.sh index 38e0f59..0b53f7c 100755 --- a/scripts/commit-check.sh +++ b/scripts/commit-check.sh @@ -38,8 +38,18 @@ check_attribution() { _msg=$1 # AI co-author / bot trailers. A human Co-authored-by is fine; the # ban is on AI vendors, bots, and *-ai-* / [bot] addresses. - if printf '%s\n' "$_msg" | grep -iE '^[[:space:]]*co-?authored-by:' \ - | grep -qiE 'claude|cursor|copilot|chatgpt|codex|gemini|devin-ai|cognition\.ai|devin\.ai[[:space:]>]|openai|anthropic|codeium|windsurf|swe-|\[bot\]|[a-z0-9-]*bot@|ai-integration'; then + _co=$(printf '%s\n' "$_msg" | grep -iE '^[[:space:]]*co-?authored-by:' || :) + # Unambiguous machine signals anywhere in the trailer: [bot] tags, + # bot@ addresses, *-ai-integration logins, and AI-vendor domains. + if printf '%s\n' "$_co" | grep -qiE '\[bot\]|[a-z0-9-]*bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code)\.(com|ai|dev|io)>'; then + err "AI Co-authored-by trailer" + # A bare vendor identity as the display name — "Co-authored-by: + # Claude " hits the domain rule above; this + # catches no-email and generic-mailbox forms. Human names that merely + # CONTAIN a vendor token ("Claude Martin ") stay + # legal per .cursor/rules/no-ai-attribution.mdc. + elif printf '%s\n' "$_co" | sed 's/^[^:]*:[[:space:]]*//; s/<.*//' \ + | grep -qiE '^(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-)([[:space:]-]ai)?[[:space:]]*$'; then err "AI Co-authored-by trailer" fi # generator footers: "Generated with ", "Made with: X", @@ -51,6 +61,11 @@ check_attribution() { if printf '%s\n' "$_msg" | grep -iqE '^[[:space:]]*(generated|made|authored)[- ](with|by|via)[[:space:]]*:'; then err "generator trailer" fi + # standalone "via " / "by " / "with " lines — the + # footer regex above needs a leading verb, so a lone via-line slips. + if printf '%s\n' "$_msg" | grep -qiE '^[[:space:]]*(via|by|using|with)[[:space:]]+(claude|cursor|copilot|chatgpt|codex|gemini|devin([[:space:]-]ai)?|openai|anthropic|codeium|windsurf|swe-[0-9a-z.-]*)[[:space:].!]*$'; then + err "attribution via-line" + fi # bot-emoji generator signature if printf '%s\n' "$_msg" | grep -q '🤖'; then err "bot-emoji attribution marker" diff --git a/src/pages/index.astro b/src/pages/index.astro index 690d946..e32c8e6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,4 +1,6 @@ --- +import { readFileSync } from "node:fs"; + import SiteShell from "@/layouts/SiteShell.astro"; import { CopyCommand } from "@/components/copy-command"; import { TerminalDemo } from "@/components/terminal-demo"; @@ -37,6 +39,13 @@ const products = [ const oxlVersion = docsLock.products.oxl.default; const oxAgentVersion = docsLock.products["ox-agent"].default; +// Landing code samples ship inside the product docs bundles — always +// released, compiled syntax, never hand-maintained site copy. +const oxlExample = readFileSync( + `src/content/docs/docs/oxl/${oxlVersion.replaceAll(".", "-")}/examples/http-server.oxl`, + "utf-8", +).trim(); + const forgePoints = [ "Repos, PRs & review, issues, orgs", "Actions CI on your own runners", @@ -336,26 +345,9 @@ const forgePoints = [
- the v0 surface + an http service
-
package main
-
-fn fail(b: bool) -> [int, err] {"{"}
-    if b {"{"} return 0, 1 {"}"}
-    return 42, nil
-{"}"}
-
-fn main() {"{"}
-    [v, err] = fail(false)
-    if err != nil {"{"} return {"}"}
-    println(itoa(v))
-
-    ch := mkchan()
-    scope {"{"}
-        spawn send(ch, 7)
-        println(itoa(recv(ch)))
-    {"}"}
-{"}"}
+
{oxlExample}
From fe60cb24778ace2d03b6aa883294dcd13378a8a4 Mon Sep 17 00:00:00 2001 From: Jesse Koldewijn Date: Thu, 12 Nov 2026 00:40:10 +0100 Subject: [PATCH 2/7] ci: retrigger Vercel preview (flake) From 6406bc0a79d373f90ed92a4ee581cabb4385ce18 Mon Sep 17 00:00:00 2001 From: Jesse Koldewijn Date: Thu, 12 Nov 2026 00:50:10 +0100 Subject: [PATCH 3/7] =?UTF-8?q?fix(site):=20restore=20index.astro=20?= =?UTF-8?q?=E2=80=94=20unrelated=20edits=20swept=20into=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uncommitted landing edits from parallel work were swept into 081392d. The new code reads examples/http-server.oxl from the synced oxl docs bundle, which only exists after the next oxl release — site and Vercel builds fail ENOENT. Restore the file to main state; the change lands in its own PR once the bundle ships. --- src/pages/index.astro | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index e32c8e6..690d946 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,6 +1,4 @@ --- -import { readFileSync } from "node:fs"; - import SiteShell from "@/layouts/SiteShell.astro"; import { CopyCommand } from "@/components/copy-command"; import { TerminalDemo } from "@/components/terminal-demo"; @@ -39,13 +37,6 @@ const products = [ const oxlVersion = docsLock.products.oxl.default; const oxAgentVersion = docsLock.products["ox-agent"].default; -// Landing code samples ship inside the product docs bundles — always -// released, compiled syntax, never hand-maintained site copy. -const oxlExample = readFileSync( - `src/content/docs/docs/oxl/${oxlVersion.replaceAll(".", "-")}/examples/http-server.oxl`, - "utf-8", -).trim(); - const forgePoints = [ "Repos, PRs & review, issues, orgs", "Actions CI on your own runners", @@ -345,9 +336,26 @@ const forgePoints = [
- an http service + the v0 surface
-
{oxlExample}
+
package main
+
+fn fail(b: bool) -> [int, err] {"{"}
+    if b {"{"} return 0, 1 {"}"}
+    return 42, nil
+{"}"}
+
+fn main() {"{"}
+    [v, err] = fail(false)
+    if err != nil {"{"} return {"}"}
+    println(itoa(v))
+
+    ch := mkchan()
+    scope {"{"}
+        spawn send(ch, 7)
+        println(itoa(recv(ch)))
+    {"}"}
+{"}"}
From 9104c0c2477e7058829bced8796cab1354972e29 Mon Sep 17 00:00:00 2001 From: Jesse Koldewijn Date: Thu, 12 Nov 2026 00:52:19 +0100 Subject: [PATCH 4/7] ci: tighten bot-suffix and via-line vendor matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bot@ signal needs a mailbox boundary — alice.talbot@ and abbot@ are human surnames, not machine identities. - ' AI' suffix applies to all vendors in the via-line rule, not just devin — 'via Claude AI' now fails. --- scripts/commit-check.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/commit-check.sh b/scripts/commit-check.sh index 0b53f7c..baffebc 100755 --- a/scripts/commit-check.sh +++ b/scripts/commit-check.sh @@ -41,7 +41,7 @@ check_attribution() { _co=$(printf '%s\n' "$_msg" | grep -iE '^[[:space:]]*co-?authored-by:' || :) # Unambiguous machine signals anywhere in the trailer: [bot] tags, # bot@ addresses, *-ai-integration logins, and AI-vendor domains. - if printf '%s\n' "$_co" | grep -qiE '\[bot\]|[a-z0-9-]*bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code)\.(com|ai|dev|io)>'; then + if printf '%s\n' "$_co" | grep -qiE '\[bot\]|(^|[<.[:space:]_-])bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code)\.(com|ai|dev|io)>'; then err "AI Co-authored-by trailer" # A bare vendor identity as the display name — "Co-authored-by: # Claude " hits the domain rule above; this @@ -63,7 +63,7 @@ check_attribution() { fi # standalone "via " / "by " / "with " lines — the # footer regex above needs a leading verb, so a lone via-line slips. - if printf '%s\n' "$_msg" | grep -qiE '^[[:space:]]*(via|by|using|with)[[:space:]]+(claude|cursor|copilot|chatgpt|codex|gemini|devin([[:space:]-]ai)?|openai|anthropic|codeium|windsurf|swe-[0-9a-z.-]*)[[:space:].!]*$'; then + if printf '%s\n' "$_msg" | grep -qiE '^[[:space:]]*(via|by|using|with)[[:space:]]+(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-[0-9a-z.-]*)([[:space:]-]ai)?[[:space:].!]*$'; then err "attribution via-line" fi # bot-emoji generator signature From 235045620905f1de56d86890bc38074980eac4c2 Mon Sep 17 00:00:00 2001 From: Jesse Koldewijn Date: Thu, 12 Nov 2026 02:00:40 +0100 Subject: [PATCH 5/7] =?UTF-8?q?ci:=20complete=20commit-check=20port=20?= =?UTF-8?q?=E2=80=94=20bot-body=20guard,=20unreachable=20before-SHA=20fetc?= =?UTF-8?q?h,=20newest=20vendor=20patterns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 16 +++++++++++++++- scripts/commit-check.sh | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ec2940..8e7f51c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: commit-lint: name: commit-lint runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v4 with: @@ -36,7 +38,11 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} run: scripts/commit-check.sh --pr-title "$PR_TITLE" - name: scan PR body for attribution markers - if: github.event_name == 'pull_request' + # Bot-authored bodies (dependabot, renovate) are platform- + # generated boilerplate — GitHub injects zero-width spaces into + # @-mentions for anti-ping, which this scan exists to catch. + # Not a human attribution surface; commits/title still lint. + if: github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' env: PR_BODY: ${{ github.event.pull_request.body }} run: | @@ -81,6 +87,14 @@ jobs: echo "new branch push — no commit range to lint" exit 0 fi + # a force-pushed before-SHA can be unreachable from every ref; + # fetch it best-effort — if it stays unresolvable the script + # fails the check rather than silently linting zero commits. + # Never --depth: a shallow boundary hides PUSH_BEFORE's + # ancestry, and rev-list would then lint all of history. The + # fetch-depth:0 checkout already has it for normal pushes — + # fetch only when absent, without a depth limit. + git cat-file -e "$PUSH_BEFORE^{commit}" 2>/dev/null || git fetch --no-tags origin "$PUSH_BEFORE" 2>/dev/null || true scripts/commit-check.sh --range "$PUSH_BEFORE..$PUSH_AFTER" # Site gate: type-aware oxlint + oxfmt/prettier check + full static build. diff --git a/scripts/commit-check.sh b/scripts/commit-check.sh index baffebc..c50dfa1 100755 --- a/scripts/commit-check.sh +++ b/scripts/commit-check.sh @@ -41,7 +41,7 @@ check_attribution() { _co=$(printf '%s\n' "$_msg" | grep -iE '^[[:space:]]*co-?authored-by:' || :) # Unambiguous machine signals anywhere in the trailer: [bot] tags, # bot@ addresses, *-ai-integration logins, and AI-vendor domains. - if printf '%s\n' "$_co" | grep -qiE '\[bot\]|(^|[<.[:space:]_-])bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code)\.(com|ai|dev|io)>'; then + if printf '%s\n' "$_co" | grep -qiE '\[bot\]|(^|[<.[:space:]_-])bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code)\.(com|ai|dev|io)([[:space:]>]|$)'; then err "AI Co-authored-by trailer" # A bare vendor identity as the display name — "Co-authored-by: # Claude " hits the domain rule above; this @@ -49,7 +49,7 @@ check_attribution() { # CONTAIN a vendor token ("Claude Martin ") stay # legal per .cursor/rules/no-ai-attribution.mdc. elif printf '%s\n' "$_co" | sed 's/^[^:]*:[[:space:]]*//; s/<.*//' \ - | grep -qiE '^(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-)([[:space:]-]ai)?[[:space:]]*$'; then + | grep -qiE '^(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-|github[[:space:]]+copilot|(claude|gemini)[[:space:]]+(opus|sonnet|pro|ultra))([[:space:]-]ai)?[[:space:]]*$'; then err "AI Co-authored-by trailer" fi # generator footers: "Generated with ", "Made with: X", From 169d324f8a9d31de2479c0038346bc6fb78f8572 Mon Sep 17 00:00:00 2001 From: Jesse Koldewijn Date: Thu, 12 Nov 2026 02:08:52 +0100 Subject: [PATCH 6/7] ci: occurrence-count markers, misorder check, footer boundary + bot-emoji context --- .github/workflows/ci.yml | 12 ++++++++++-- scripts/commit-check.sh | 10 +++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e7f51c..e28144a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,8 +54,10 @@ jobs: # a stray start marker could hide attribution after it. open_m='' close_m='' - opens=$(grep -cF "$open_m" pr-body.txt || :) - closes=$(grep -cF "$close_m" pr-body.txt || :) + # -oF counts occurrences, not matching lines — same-line + # pairs would read as one open and one close to -c. + opens=$(grep -oF "$open_m" pr-body.txt | wc -l || :) + closes=$(grep -oF "$close_m" pr-body.txt | wc -l || :) if [ "$opens" != "$closes" ]; then echo "commit-check: unbalanced CodeRabbit release-notes markers in PR body" >&2 exit 1 @@ -76,6 +78,12 @@ jobs: } printf "%s", out rest }' pr-body.txt > pr-body-clean.txt + # equal counts but wrong order leave markers behind in the + # cleaned body — a stray open/close is not a managed block. + if grep -qF "$open_m" pr-body-clean.txt || grep -qF "$close_m" pr-body-clean.txt; then + echo "commit-check: misordered CodeRabbit release-notes markers in PR body" >&2 + exit 1 + fi scripts/commit-check.sh --scan-file pr-body-clean.txt - name: lint push range if: github.event_name == 'push' diff --git a/scripts/commit-check.sh b/scripts/commit-check.sh index c50dfa1..284aded 100755 --- a/scripts/commit-check.sh +++ b/scripts/commit-check.sh @@ -55,7 +55,9 @@ check_attribution() { # generator footers: "Generated with ", "Made with: X", # "Authored via X" — the tool names that sign output, plus the # trailer forms regardless of vendor. - if printf '%s\n' "$_msg" | grep -iqE '(generated|made|created|authored|written|produced)[[:space:]]+(with|by|via)[[:space:]]*\[?[[:space:]]*(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-)'; then + # connector may carry a colon ("Created with: Cursor"); the vendor + # token needs a non-alnum tail so "generated by cursors" stays clean. + if printf '%s\n' "$_msg" | grep -iqE '(generated|made|created|authored|written|produced)[[:space:]]+(with|by|via):?[[:space:]]*\[?[[:space:]]*(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-[0-9a-z.-]*)([^a-z0-9]|$)'; then err "AI generator footer" fi if printf '%s\n' "$_msg" | grep -iqE '^[[:space:]]*(generated|made|authored)[- ](with|by|via)[[:space:]]*:'; then @@ -66,8 +68,10 @@ check_attribution() { if printf '%s\n' "$_msg" | grep -qiE '^[[:space:]]*(via|by|using|with)[[:space:]]+(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-[0-9a-z.-]*)([[:space:]-]ai)?[[:space:].!]*$'; then err "attribution via-line" fi - # bot-emoji generator signature - if printf '%s\n' "$_msg" | grep -q '🤖'; then + # bot-emoji signature: a bare 🤖 line, or 🤖 sharing a line with + # attribution vocabulary — a mid-sentence robot mention stays clean. + _bot=$(printf '%s\n' "$_msg" | grep '🤖' || :) + if printf '%s\n' "$_bot" | grep -qiE '^[[:space:]]*🤖[[:space:]]*$|generated|made|created|authored|written|produced|built|co-?authored|via[[:space:]]|with[[:space:]]|by[[:space:]]'; then err "bot-emoji attribution marker" fi # AI named as the author/agent — not as the subject of the change. From f76c656bf9acf2e4f5b855ec72676f072cad214c Mon Sep 17 00:00:00 2001 From: Jesse Koldewijn Date: Thu, 12 Nov 2026 02:46:28 +0100 Subject: [PATCH 7/7] ci: catch vendor tokens in bare co-author emails and glued -ai names --- scripts/commit-check.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/commit-check.sh b/scripts/commit-check.sh index 284aded..39e3b69 100755 --- a/scripts/commit-check.sh +++ b/scripts/commit-check.sh @@ -36,12 +36,19 @@ warn() { printf '%s\n' "commit-check: warning: $*" >&2; } check_attribution() { _msg=$1 + # One vendor alternation shared by every pattern below — extend it + # here, never in a single rule, or the rules drift apart. + _VENDOR='claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|github[[:space:]]+copilot|claude[[:space:]]+code|(claude|gemini)[[:space:]]+(opus|sonnet|pro|ultra)|swe-[0-9a-z.-]*' # AI co-author / bot trailers. A human Co-authored-by is fine; the # ban is on AI vendors, bots, and *-ai-* / [bot] addresses. _co=$(printf '%s\n' "$_msg" | grep -iE '^[[:space:]]*co-?authored-by:' || :) # Unambiguous machine signals anywhere in the trailer: [bot] tags, - # bot@ addresses, *-ai-integration logins, and AI-vendor domains. - if printf '%s\n' "$_co" | grep -qiE '\[bot\]|(^|[<.[:space:]_-])bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code)\.(com|ai|dev|io)([[:space:]>]|$)'; then + # bot@ addresses, *-ai-integration logins, AI-vendor domains (literal + # list of vendor-owned domains — never github.com, or humans on the + # privacy relay get flagged), and a vendor token inside a bare email + # (no ) — bot forms like + # "Co-authored-by: 175728472+Copilot@users.noreply.github.com". + if printf '%s\n' "$_co" | grep -qiE '\[bot\]|(^|[<.[:space:]_+-])bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code|cursor|gemini|chatgpt|codex|copilot|google)\.(com|ai|dev|io)([[:space:]>]|$)|:[[:space:]]*[^<[:space:]]*('"$_VENDOR"')[^<[:space:]]*@'; then err "AI Co-authored-by trailer" # A bare vendor identity as the display name — "Co-authored-by: # Claude " hits the domain rule above; this @@ -49,7 +56,7 @@ check_attribution() { # CONTAIN a vendor token ("Claude Martin ") stay # legal per .cursor/rules/no-ai-attribution.mdc. elif printf '%s\n' "$_co" | sed 's/^[^:]*:[[:space:]]*//; s/<.*//' \ - | grep -qiE '^(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-|github[[:space:]]+copilot|(claude|gemini)[[:space:]]+(opus|sonnet|pro|ultra))([[:space:]-]ai)?[[:space:]]*$'; then + | grep -qiE '^\[?('"$_VENDOR"')\]?(\([^)]*\))?([[:space:]-]?ai)?[[:space:]]*$'; then err "AI Co-authored-by trailer" fi # generator footers: "Generated with ", "Made with: X", @@ -57,7 +64,7 @@ check_attribution() { # trailer forms regardless of vendor. # connector may carry a colon ("Created with: Cursor"); the vendor # token needs a non-alnum tail so "generated by cursors" stays clean. - if printf '%s\n' "$_msg" | grep -iqE '(generated|made|created|authored|written|produced)[[:space:]]+(with|by|via):?[[:space:]]*\[?[[:space:]]*(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-[0-9a-z.-]*)([^a-z0-9]|$)'; then + if printf '%s\n' "$_msg" | grep -iqE '(generated|made|created|authored|written|produced)[[:space:]]+(with|by|via):?[[:space:]]*\[?[[:space:]]*('"$_VENDOR"')([^a-z0-9]|$)'; then err "AI generator footer" fi if printf '%s\n' "$_msg" | grep -iqE '^[[:space:]]*(generated|made|authored)[- ](with|by|via)[[:space:]]*:'; then @@ -65,7 +72,7 @@ check_attribution() { fi # standalone "via " / "by " / "with " lines — the # footer regex above needs a leading verb, so a lone via-line slips. - if printf '%s\n' "$_msg" | grep -qiE '^[[:space:]]*(via|by|using|with)[[:space:]]+(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-[0-9a-z.-]*)([[:space:]-]ai)?[[:space:].!]*$'; then + if printf '%s\n' "$_msg" | grep -qiE '^[[:space:]]*(via|by|using|with)[[:space:]]+\[?('"$_VENDOR"')\]?(\([^)]*\))?([[:space:]-]?ai)?([^a-z0-9]|$)'; then err "attribution via-line" fi # bot-emoji signature: a bare 🤖 line, or 🤖 sharing a line with @@ -76,11 +83,11 @@ check_attribution() { fi # AI named as the author/agent — not as the subject of the change. # (kept narrow on purpose: "add Copilot webhook" must stay legal) - if printf '%s\n' "$_msg" | grep -iqE '\b(as an ai|i generated this|authored by (claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|swe-))\b'; then + if printf '%s\n' "$_msg" | grep -iqE '\b(as an ai|i generated this|authored by \[?('"$_VENDOR"'))\b'; then err "AI self-attribution" fi # vendor-stamped HTML comments smuggled into the message - if printf '%s\n' "$_msg" | grep -iqE '