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
1 change: 1 addition & 0 deletions .claude/skills
1 change: 1 addition & 0 deletions .gemini/skills
2 changes: 1 addition & 1 deletion .github/scripts/answer_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def main():
api_key = os.getenv("GEMINI_API_KEY")
issue_title = os.getenv("ISSUE_TITLE")
issue_body = os.getenv("ISSUE_BODY")
skill_file = os.getenv("SKILL_FILE", ".gemini/skills/android-maps-compose/SKILL.md")
skill_file = os.getenv("SKILL_FILE", ".agents/skills/android-maps-compose/SKILL.md")
response_file = os.getenv("RESPONSE_FILE", "issue_response.md")

if not api_key:
Expand Down
172 changes: 172 additions & 0 deletions .github/scripts/check_agent_leftovers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
#
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Fail a pull request that carries leftovers from local or agent-assisted work.

Only what the pull request adds is checked: added lines of the diff, the
names of added files, the messages of its commits, and its title and body.
Existing content on the base branch is never flagged.

Things that are fine on the author's machine but wrong in a public repo:

local paths file:// URLs into home or temp directories, and home
directories themselves (/Users/x/, /home/x/,
/usr/local/google/, C:\\Users\\x\\)
internal links go/ short links, b/ bug IDs, corp and googleplex hosts
secrets Google API keys
agent traces AI co-author trailers, links to agent chat sessions
local files agent settings meant to stay local, merge leftovers

A line that legitimately needs one of these can opt out by containing
"agent-leftovers: ignore".

Usage, from the repository root:

BASE_SHA=<base commit> [HEAD_SHA=<head, default HEAD>] [PR_TITLE=...] [PR_BODY=...] \\
check_agent_leftovers.py

Findings are printed as GitHub Actions error annotations.
"""

import os
import re
import subprocess
import sys

IGNORE_MARKER = "agent-leftovers: ignore"

# This script and its workflow describe the patterns, so they would match them.
SKIPPED_FILES = {
".github/scripts/check_agent_leftovers.py",
".github/workflows/agent-leftovers.yml",
}

CONTENT_PATTERNS = [
("local file URL", re.compile(r"file:///(Users|home|usr/local/google|private/var|var/folders|[A-Za-z]:)")),
("local path", re.compile(r"/usr/local/google/")),
("local path", re.compile(r"(?<![\w.])/Users/[\w.-]+/")),
("local path", re.compile(r"(?<![\w.])/home/(?!runner/)[\w.-]+/")),
("local path", re.compile(r"\b[A-Za-z]:\\+Users\\+[\w.-]+")),
("internal short link", re.compile(r"(?<![\w./-])go/[A-Za-z0-9][\w.-]*")),
("internal short link", re.compile(r"https?://go/")),
("internal bug ID", re.compile(r"(?<![\w./-])b/\d{6,}")),
("internal host", re.compile(r"\b[\w.-]+\.corp\.google\.com\b")),
("internal host", re.compile(r"\bgoogleplex\.com\b")),
("Google API key", re.compile(r"AIza[0-9A-Za-z_-]{35}")),
("agent session link", re.compile(r"claude\.ai/(code/)?(chat|session)")),
("agent session link", re.compile(r"chatgpt\.com/(c|share)/")),
("agent session link", re.compile(r"gemini\.google\.com/(app|share)/")),
("agent attribution", re.compile(r"Generated with \[?(Claude|Gemini|Copilot|Codex|Cursor)", re.I)),
(
"AI co-author trailer",
re.compile(r"Co-Authored-By:.*\b(Claude|Anthropic|Gemini|Copilot|Codex|Cursor|OpenAI)\b", re.I),
),
]

FILE_PATTERNS = [
("local agent settings", re.compile(r"(^|/)\.claude/settings\.local\.json$")),
("local agent memory", re.compile(r"(^|/)CLAUDE\.local\.md$")),
("merge leftover", re.compile(r"\.(orig|rej)$")),
]


def git(*args):
return subprocess.run(
["git", *args], check=True, capture_output=True, text=True
).stdout


def scan_text(text):
"""Yield the description of every pattern found in text, line by line."""
for line in text.splitlines():
if IGNORE_MARKER in line:
continue
for name, pattern in CONTENT_PATTERNS:
match = pattern.search(line)
if match:
yield f"{name}: {match.group(0)}"


def added_lines(base, head):
"""Yield (path, line number, text) for every line the diff adds."""
diff = git("diff", "--unified=0", "--no-color", "--no-ext-diff", f"{base}...{head}")
path = None
line_no = 0
for line in diff.splitlines():
if line.startswith("+++ "):
path = line[6:] if line.startswith("+++ b/") else None
elif line.startswith("@@"):
hunk = re.match(r"@@ -\S+ \+(\d+)", line)
line_no = int(hunk.group(1)) if hunk else 0
elif line.startswith("+") and path:
yield path, line_no, line[1:]
line_no += 1


def annotate(message, path=None, line=None):
location = ""
if path:
location = f" file={path}" + (f",line={line}" if line else "")
print(f"::error{location}::{message}")


def main():
base = os.environ.get("BASE_SHA")
head = os.environ.get("HEAD_SHA", "HEAD")
if not base:
print("BASE_SHA is not set.", file=sys.stderr)
return 2

findings = 0

for path, line_no, text in added_lines(base, head):
if path in SKIPPED_FILES:
continue
for finding in scan_text(text):
annotate(finding, path, line_no)
findings += 1

for path in git("diff", "--name-only", "--diff-filter=A", f"{base}...{head}").splitlines():
for name, pattern in FILE_PATTERNS:
if pattern.search(path):
annotate(f"{name}: {path} should not be committed", path)
findings += 1

for entry in git("log", "--no-merges", "--format=%h%x00%B%x1e", f"{base}..{head}").split("\x1e"):
if not entry.strip():
continue
sha, _, message = entry.strip().partition("\x00")
for finding in scan_text(message):
annotate(f"commit {sha} message has {finding}")
findings += 1

for field in ("PR_TITLE", "PR_BODY"):
for finding in scan_text(os.environ.get(field, "")):
annotate(f"pull request {field[3:].lower()} has {finding}")
findings += 1

if findings:
print(
f"\nFound {findings} leftover(s). Remove them, or add "
f'"{IGNORE_MARKER}" to a line that legitimately needs one.'
)
return 1
print("No agent leftovers found.")
return 0


if __name__ == "__main__":
sys.exit(main())
2 changes: 1 addition & 1 deletion .github/scripts/update_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def get_gemini_response(api_key, prompt):
def main():
api_key = os.getenv("GEMINI_API_KEY")
diff_file = os.getenv("DIFF_FILE", "release_diff.patch")
skill_file = os.getenv("SKILL_FILE", ".gemini/skills/android-maps-compose/SKILL.md")
skill_file = os.getenv("SKILL_FILE", ".agents/skills/android-maps-compose/SKILL.md")

if not api_key:
print("GEMINI_API_KEY not found", file=sys.stderr)
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/agent-leftovers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Fails a pull request that adds local paths, internal links, API keys or
# agent traces. See .github/scripts/check_agent_leftovers.py for the list.
name: Agent leftovers

on:
pull_request:
types: [opened, synchronize, reopened, edited]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout Repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Check for agent leftovers
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: python3 .github/scripts/check_agent_leftovers.py
4 changes: 2 additions & 2 deletions .github/workflows/release-rc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
sed -i 's/version = "[^"]*"/version = "'"$VERSION"'"/' build.gradle.kts

# Files with x-release-please-version inline marker
for file in README.md llm-integration-prompt.md .gemini/skills/android-maps-compose/SKILL.md; do
for file in README.md llm-integration-prompt.md .agents/skills/android-maps-compose/SKILL.md; do
if [ -f "$file" ]; then
sed -i "/x-release-please-version/s/[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[a-zA-Z0-9.-]*/$VERSION/" "$file"
fi
Expand All @@ -70,7 +70,7 @@ jobs:
build.gradle.kts \
README.md \
llm-integration-prompt.md \
.gemini/skills/android-maps-compose/SKILL.md
.agents/skills/android-maps-compose/SKILL.md
git commit -m "chore: release v${VERSION}"
git push origin "$BRANCH"

Expand Down
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ Running `maps-app` requires a Maps API key: put `MAPS_API_KEY=...` in
`secrets.properties` at the repo root (see `local.defaults.properties` for the
template). Never hardcode or commit API keys.

## Agent setup

This file is the shared instruction set for every coding agent. `CLAUDE.md`
and `GEMINI.md` only import it, and Codex and Copilot read it directly, so put
new guidance here rather than in a tool-specific file.

Skills live in `.agents/skills/`. `.claude/skills` and `.gemini/skills` are
symlinks to that folder, so edit the files under `.agents/skills/` only.
`.agents/skills/android-maps-compose/SKILL.md` teaches agents how apps use this
library: release-please bumps its version lines, and the `Update Skill`
workflow refreshes its content after each release.

`.gemini/config.yaml` and `.gemini/styleguide.md` configure Gemini Code Assist
reviews on pull requests, so they stay specific to that tool.

## Code style

- Adhere to formatting rules defined in `.editorconfig`.
Expand Down Expand Up @@ -117,6 +132,18 @@ template). Never hardcode or commit API keys.

The PR title still has to be a valid Conventional Commit. Each line counts
for the version bump, so a `feat:` or `!` line bumps accordingly.
- Keep a pull request to what its title says. Once review has started, put
new features or unrelated fixes in a separate pull request. If the scope
does change, update the type in the title (a `refactor:` that now adds
behavior is a `feat:`), since release-please reads it.
- Answer every review thread before asking for another review: push the
change, or reply in the thread with why not. A new commit alone does not
tell the reviewer which comments it addresses.
- Never commit anything that only makes sense on your machine or inside
Google: local paths (`file:///`, `/Users/...`, workstation home
directories), `go/` links, `b/` bug IDs or API keys. The `Agent leftovers` check fails
the pull request on these, and on AI co-author trailers and links to agent
chat sessions.
- Every behavior change needs a unit test in the affected module.
- All pull requests are to be created as drafts (`gh pr create --draft`) until authorization is explicitly given to mark them ready for review. Always inform the user that the PR was created as a draft.
- Run the module's tests and `lint` before declaring work done, and report
Expand Down
1 change: 1 addition & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
2 changes: 1 addition & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"build.gradle.kts",
"README.md",
"llm-integration-prompt.md",
".gemini/skills/android-maps-compose/SKILL.md"
".agents/skills/android-maps-compose/SKILL.md"
]
}
}
Expand Down
Loading