diff --git a/.claude/settings.json b/.claude/settings.json index e61a9fc3a4..6d42c6885b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -33,6 +33,24 @@ "command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/no-strict-ignore-new-file.sh\"" } ] + }, + { + "matcher": "mcp__github__(add_issue_comment|add_comment_to_pending_review|add_reply_to_pull_request_comment|pull_request_review_write|issue_write|sub_issue_write)", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/github-comment-style.sh\"" + } + ] + }, + { + "matcher": "mcp__github__create_pull_request", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/pr-template-blank.sh\"" + } + ] } ], "Stop": [ diff --git a/.codex/config.toml b/.codex/config.toml index e85f9bb69c..890672ea71 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -42,6 +42,27 @@ command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/format-edited-f type = "command" command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/prefer-one-component.sh"' +# Require GitHub comments, reviews and issues to be prefixed with the robot emoji +# 🤖. Matches the github MCP comment/review/issue writers below (add_issue_comment, +# issue_write, â€Ļ); the guard reads tool_input.body and, for issues, +# tool_input.title (the same payload Claude passes), and blocks an un-prefixed +# comment body or issue title via exit 2. +[[hooks.PreToolUse]] +matcher = "^mcp__github__(add_issue_comment|add_comment_to_pending_review|add_reply_to_pull_request_comment|pull_request_review_write|issue_write|sub_issue_write)$" +[[hooks.PreToolUse.hooks]] +type = "command" +command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/github-comment-style.sh"' +statusMessage = "Checking GitHub comment/issue is 🤖-prefixed" + +# When creating a PR, require the body to be the blank PR template (unmodified) +# — agents must not fill it in. Reads tool_input.body; blocks via exit 2. +[[hooks.PreToolUse]] +matcher = "^mcp__github__create_pull_request$" +[[hooks.PreToolUse.hooks]] +type = "command" +command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/pr-template-blank.sh"' +statusMessage = "Checking PR template is left blank" + # When the turn ends, typecheck + test the workspaces this branch touched. [[hooks.Stop]] [[hooks.Stop.hooks]] diff --git a/.cursor/hooks.json b/.cursor/hooks.json index 724d24d7a1..0a7f665c01 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -2,6 +2,9 @@ "version": 1, "hooks": { "beforeShellExecution": [{ "command": "./.cursor/hooks/guard-shell.sh" }], + "beforeMCPExecution": [ + { "command": "./.cursor/hooks/before-mcp-github.sh" } + ], "afterFileEdit": [{ "command": "./.cursor/hooks/after-file-edit.sh" }] } } diff --git a/.cursor/hooks/before-mcp-github.sh b/.cursor/hooks/before-mcp-github.sh new file mode 100755 index 0000000000..25fb943ae6 --- /dev/null +++ b/.cursor/hooks/before-mcp-github.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# Cursor `beforeMCPExecution` adapter. +# +# Dispatches github MCP calls to the matching shared guard in +# scripts/agent-hooks/: comment/review/issue writers must be 🤖-prefixed +# (github-comment-style.sh), and create_pull_request must leave the PR template +# blank (pr-template-blank.sh). +# Cursor input on stdin: { tool_name, tool_input, ... }. Output on stdout: +# { permission: "allow" | "deny", userMessage, agentMessage }. + +input=$(cat) +# `CDPATH= cd` is an intentional empty-env prefix (not an assignment); silence the +# SC1007 false positive shellcheck raises for it. +# shellcheck disable=SC1007 +ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd) + +allow() { + printf '%s\n' '{"permission":"allow"}' + exit 0 +} +deny() { + jq -n --arg m "$1" \ + '{permission:"deny", userMessage:"Blocked by repo policy", agentMessage:$m}' + exit 0 +} + +# Pick the guard for this tool; wave known non-GitHub tools by. Fail closed on a +# malformed payload (jq parse failure / missing .tool_name), matching +# guard-shell.sh — a payload we can't read must not bypass the GitHub guards. +tool=$(printf '%s' "$input" | jq -re '.tool_name' 2>/dev/null) || + deny "Invalid Cursor hook payload: could not read .tool_name." +case "$tool" in + mcp__github__add_issue_comment | \ + mcp__github__add_comment_to_pending_review | \ + mcp__github__add_reply_to_pull_request_comment | \ + mcp__github__pull_request_review_write | \ + mcp__github__issue_write | \ + mcp__github__sub_issue_write) + guard=github-comment-style.sh ;; + mcp__github__create_pull_request) + guard=pr-template-blank.sh ;; + *) allow ;; +esac + +# Each guard reads the same `.tool_input.*` payload Cursor passes, so forward it +# unchanged. Exit 0 allows, exit 2 + stderr is a real policy denial. Fail closed +# on any other exit (matching guard-shell.sh) so a broken/missing guard can't +# silently disable enforcement; the message marks it as an execution problem. +# (Each guard fails closed on an unreadable payload and exit-0s only when there's +# genuinely nothing to enforce.) +status=0 +err=$(printf '%s' "$input" | "$ROOT/scripts/agent-hooks/$guard" 2>&1) || status=$? +case "$status" in + 0) allow ;; + 2) deny "$err" ;; + *) deny "$guard failed (exit $status): $err" ;; +esac diff --git a/.cursor/rules/pr-and-commit.mdc b/.cursor/rules/pr-and-commit.mdc index 5994ebbcbb..ec9375cced 100644 --- a/.cursor/rules/pr-and-commit.mdc +++ b/.cursor/rules/pr-and-commit.mdc @@ -14,6 +14,3 @@ listed here. - **ALL pull request titles MUST be prefixed with `[AI]`.** This isn't enforced automatically — you have to apply it yourself. -- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`) — leave it - blank for the human. Exception: if a human explicitly asks, fill it out **in - Chinese** (įŽ€äŊ“中文). diff --git a/.github/agents/pr-and-commit-rules.md b/.github/agents/pr-and-commit-rules.md index 9f4d6a05fa..7361cb5306 100644 --- a/.github/agents/pr-and-commit-rules.md +++ b/.github/agents/pr-and-commit-rules.md @@ -1,14 +1,13 @@ # PR and Commit Rules for AI Agents -The mechanical commit and git rules are handled by tooling. This file lists only -the rules you have to apply yourself. +This file lists the PR and commit rules you have to apply yourself. ## Pull Request Rules ### `[AI]` prefix on PR titles -**ALL pull request titles MUST be prefixed with `[AI]`.** This isn't enforced -automatically — you have to apply it yourself. +**ALL pull request titles MUST be prefixed with `[AI]`** — you have to apply it +yourself. **Examples:** @@ -17,8 +16,24 @@ automatically — you have to apply it yourself. ### Do not fill in the PR template -- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all - blank spaces and placeholder comments as-is — the human who tested the change - fills in the Description, Related issue(s), Testing, and Checklist sections. +- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Create + the PR with that template as the body, unmodified — leave all blank spaces and + placeholder comments as-is, and leave every checklist box unchecked. The human + who tested the change fills in the Description, Related issue(s), Testing, and + Checklist sections. - **Exception**: if a human **explicitly asks** you to fill it out, do so **in Chinese**, using Chinese characters (įŽ€äŊ“中文) for all content you add. + +## GitHub comment, review and issue prefix + +**Prefix everything you post to GitHub with the robot emoji 🤖** — pull-request +and issue comments, pull-request reviews (including inline review comments), and +the title and body of issues you create. This keeps agent-authored content +visibly marked. It does **not** change PR titles (still `[AI] â€Ļ`) or commit +messages (still `[AI] â€Ļ`). + +Write the text normally; just make sure 🤖 is the first character (for issues, +on both the title and the body). + +This applies only to comments **you** author — bots like CodeRabbit post under +their own identity and are not affected. diff --git a/AGENTS.md b/AGENTS.md index 9717bf7c0e..53b72f04bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,8 +44,8 @@ yarn start:desktop ### âš ī¸ PR titles must start with `[AI]` -Every pull request title must be prefixed with `[AI]` — this isn't enforced -automatically, so you have to apply it yourself. See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md). +Every pull request title must be prefixed with `[AI]` — you have to apply it +yourself. See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md). ### Task Orchestration with Lage @@ -357,7 +357,11 @@ describe('ComponentName', () => { - `/README.md` - Project overview - `/CONTRIBUTING.md` - Points to community docs -- `/upcoming-release-notes/` - Release notes for next version +- `/upcoming-release-notes/` - Release notes for next version. Name each file + with a short, descriptive slug (e.g. `add-payee-autocomplete.md`) — the PR link + is resolved automatically at release time, so you don't need the PR number. + Numeric filenames like `1234.md` also remain valid. See the release-note + template and rules in `packages/docs/docs/contributing/index.md`. - `/CODEOWNERS` - Code ownership definitions - `/packages/docs/` - Documentation website (Docusaurus) @@ -506,7 +510,7 @@ Before committing changes, ensure: ## Pull Request Guidelines -See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, and PR template handling. +See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, the GitHub comment/review/issue 🤖 prefix, and PR template handling. ## Code Review Guidelines diff --git a/scripts/agent-hooks/github-comment-style.sh b/scripts/agent-hooks/github-comment-style.sh new file mode 100755 index 0000000000..6a5f9019f4 --- /dev/null +++ b/scripts/agent-hooks/github-comment-style.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Shared agent guard for GitHub comments / reviews / issues, wired for Claude +# (PreToolUse), Codex (PreToolUse) and Cursor (beforeMCPExecution via adapter). +# +# Every comment, review or issue an agent posts to GitHub must be prefixed with +# the robot emoji 🤖, so agent-authored content is always visibly marked. Reads +# the text from `.tool_input.body` (and `.tool_input.title` for issues) on +# stdin; each non-empty field must start with 🤖 (after any leading whitespace), +# otherwise the call is blocked (exit 2 + stderr) so the agent fixes it. +# +# This only ever runs on the agent's own outgoing comments — bots like CodeRabbit +# post under their own identity and never pass through this hook. + +block() { + echo "$1" >&2 + exit 2 +} + +input=$(cat) + +# 🤖 = U+1F916, UTF-8 F0 9F A4 96. Build it from raw bytes so the prefix check is +# locale- and file-encoding-independent. +robot=$(printf '\360\237\244\226') + +# Fail closed on a malformed payload — invalid JSON, or a missing/non-object +# `.tool_input` — matching git-guard.sh / guard-shell.sh, so a payload we can't +# read can't silently bypass the prefix check. A genuinely absent body/title +# within a valid `.tool_input` is fine (nothing to mark). +fields=$(printf '%s' "$input" | jq -e '.tool_input | objects' 2>/dev/null) || + block "Blocked: could not read the hook payload (.tool_input). (scripts/agent-hooks/github-comment-style.sh)" +body=$(printf '%s' "$fields" | jq -r '.body // empty') +title=$(printf '%s' "$fields" | jq -r '.title // empty') + +# A field passes if it's empty/whitespace-only (nothing to mark) or, after +# stripping leading whitespace, begins with the robot emoji. +starts_with_robot() { + # ${1%%[![:space:]]*} is the leading whitespace; strip it off the front. A + # whitespace-only (or empty) field strips down to "" and passes too. + trimmed=${1#"${1%%[![:space:]]*}"} + case "$trimmed" in + "" | "$robot"*) return 0 ;; + *) return 1 ;; + esac +} + +starts_with_robot "$title" || + block "Blocked: GitHub issue titles must start with the robot emoji 🤖 to mark them as agent-authored. Prefix the title with 🤖 and try again. (scripts/agent-hooks/github-comment-style.sh)" +starts_with_robot "$body" || + block "Blocked: GitHub comments, reviews and issues must start with the robot emoji 🤖 to mark them as agent-authored. Prefix the body with 🤖 and try again. (scripts/agent-hooks/github-comment-style.sh)" + +exit 0 diff --git a/scripts/agent-hooks/pr-template-blank.sh b/scripts/agent-hooks/pr-template-blank.sh new file mode 100755 index 0000000000..332ade1bfb --- /dev/null +++ b/scripts/agent-hooks/pr-template-blank.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# Shared agent guard for pull-request creation, wired for Claude (PreToolUse), +# Codex (PreToolUse) and Cursor (beforeMCPExecution via adapter). +# +# Agents must NOT fill in the PR template: when creating a pull request the body +# must be the repo's blank template (.github/PULL_REQUEST_TEMPLATE.md), +# unmodified, so the human who tested the change fills in the Description, +# Testing and Checklist sections (AGENTS.md / pr-and-commit-rules.md). Reads +# `.tool_input.body` on stdin and blocks (exit 2 + stderr) if it isn't the +# pristine template. +# +# Comparison ignores cosmetic differences only: CR/LF, trailing whitespace per +# line, and leading/trailing blank lines. Any real content (a filled-in section, +# a checked box, extra prose) makes the body differ and is blocked. + +block() { + echo "$1" >&2 + exit 2 +} + +input=$(cat) + +# Resolve the repo root regardless of which agent invoked us, then the template. +. "$(dirname "$0")/common.sh" +ROOT=$(resolve_repo_root) +template="$ROOT/.github/PULL_REQUEST_TEMPLATE.md" + +# If we can't find the template, fail open — never block PR creation just because +# the comparison baseline is missing. +[ -f "$template" ] || exit 0 + +# Fail open when there's no body to compare: a parse failure or an absent/null +# `.tool_input.body` must not wedge PR creation. (The Cursor/Codex/Claude wiring +# still fails closed if THIS script can't execute at all.) `jq -re` exits +# non-zero for both cases; an empty-string body, however, is a real submission +# and still falls through to the template check below. +body=$(printf '%s' "$input" | jq -re '.tool_input.body' 2>/dev/null) || exit 0 + +# Canonicalise: drop CR, strip per-line trailing whitespace, and trim leading and +# trailing blank lines, so only meaningful content differences remain. +canon() { + printf '%s\n' "$1" | awk ' + { sub(/[[:space:]]+$/, ""); lines[NR] = $0 } # also strips a trailing CR + END { + s = 1; e = NR + while (s <= e && lines[s] == "") s++ + while (e >= s && lines[e] == "") e-- + for (i = s; i <= e; i++) print lines[i] + }' +} + +[ "$(canon "$body")" = "$(canon "$(cat "$template")")" ] && exit 0 + +block "Blocked: don't fill in the PR template. Create the pull request with the repo's blank template (.github/PULL_REQUEST_TEMPLATE.md) as the body, unmodified — the human who tested the change fills in the Description, Testing and Checklist sections (AGENTS.md). (scripts/agent-hooks/pr-template-blank.sh)" diff --git a/upcoming-release-notes/github-comment-robot-emoji-prefix.md b/upcoming-release-notes/github-comment-robot-emoji-prefix.md new file mode 100644 index 0000000000..b07cf03410 --- /dev/null +++ b/upcoming-release-notes/github-comment-robot-emoji-prefix.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [MatissJanis] +--- + +Prefix agent-authored GitHub comments, reviews and issues with the robot emoji 🤖.