AGENTS.md · diff

git:20260905.4dea3f6 to git:20260905.e60a9e4

1 added, 0 removed. Audit A to A.

# AGENTS.md
The instruction file every coding agent working on this repository reads. It is
deliberately short and deliberately duplicated nowhere: it carries the rules that
are **enforced by CI**, plus a map to where everything else lives.
Keep it small. Codex CLI embeds at most `project_doc_max_bytes` of this file into
its first-turn instructions — **32 KiB by default**, silently truncating the rest
— and every agent pays for it on every turn. Detail, rationale and worked
examples belong in [`docs/AGENT_GUIDE.md`](docs/AGENT_GUIDE.md) and the rest of
[`docs/`](docs/README.md), which agents read on demand. How each agent resolves
its way here, and what that costs, is measured in
[`docs/AGENT_ENTRYPOINTS.md`](docs/AGENT_ENTRYPOINTS.md).
## What this project is
A Gradle plugin (`ee.schimke.composeai.preview`) plus supporting tools that
discover `@Preview` composables in compiled Kotlin classes and render them to PNG
outside Android Studio — Jetpack Compose (Android, via Robolectric) and Compose
Multiplatform Desktop (via `ImageComposeScene`). Architecture, commands, state
seams and the load-bearing constraints: [`docs/AGENT_GUIDE.md`](docs/AGENT_GUIDE.md).
## The CI-enforced invariants
Break one of these and the pull request goes red. They are stated here, once, and
cited — never restated — everywhere else.
<!-- invariant: agent-attribution -->
### Git history must never attribute work to an AI agent
Non-negotiable. Commits are attributed solely to the human committer. Two things
are rejected:
1. a `Co-authored-by:` trailer, at trailer position, naming an agent (Claude /
Codex / ChatGPT / Copilot / Gemini) or using an agent email; and
2. a commit whose **author or committer identity** is an agent — an
`@anthropic.com` / `@openai.com` email, or an agent **name** set as the
identity.
GitHub App bot accounts (`…[bot]`, e.g. `claude[bot]`) are exempt from (2) but
not from (1). Explicitly **fine**, and intentionally not matched: links to an
agent session (`claude.ai/code`), the "🤖 Generated with …" and
`_Generated by [Claude Code]_` footers, and ordinary PR comments — none of those
claim authorship.
**Scrub the PR description too, not just the commits.** The gate scans the PR
title and body, so a `Co-authored-by:` line there fails CI.
Enforced in three places sharing one detector
([`.github/scripts/agent-attribution-scan.sh`](.github/scripts/agent-attribution-scan.sh))
so they cannot disagree: the local [`commit-msg`](.githooks/commit-msg) hook, the
local [`pre-push`](.githooks/pre-push) hook (which catches amended, rebased,
cherry-picked or pre-hook commits `commit-msg` never saw), and the
[`No Agent Attribution`](.github/workflows/no-agent-attribution.yml) CI gate.
Install the hooks with [`scripts/install-git-hooks.sh`](scripts/install-git-hooks.sh).
Claude Code's `SessionStart` hook runs it for you; **every other agent has to run
it itself** — that hook lives in `.claude/settings.json` and never fires in a
Codex, Gemini or Copilot session, so those clones have no local check at all
until someone runs the script. `--no-verify` bypasses the local hooks; CI
still won't. Why the PR body matters and how a trailer reached `main` anyway:
[`docs/AGENT_GUIDE.md` → Git conventions](docs/AGENT_GUIDE.md#git-conventions).
**Reviewers: run the detector before reporting an attribution finding.** This is
the most-reported and least-real finding on this repo — 14 PRs drew a P1 "the
author and committer are both `Codex`" comment over 2026-08-31/09-01 and not one
was real. An agent reviewing its own or another agent's PR must not infer the
commit identity from the fact that an agent wrote the code: the identity is
whatever `git config user.email` held, and here that is the human. Check it, on
the real range:
```
git log --format='%h %an <%ae> | %cn <%ce>' <base>..<head>
.github/scripts/agent-attribution-scan.sh --range '<base>..<head>'
```
`Yuri Schimke <yuri@schimke.ee>` is this repository's human identity, and
`github-actions[bot]` / `renovate[bot]` are exempt bot accounts (see above) — none
of the three is ever a finding. Report only what the scanner exits 1 on, quote its
output, and if the `Reject agent attribution` check is already green on the head
commit there is nothing to report.
<!-- invariant: branch-prefix -->
### Branch names are always `agent/…`
Never `claude/…`, `copilot/…`, `codex/…`, `cursor/…`, or any other agent prefix.
Don't ask which prefix to use — it is always `agent/`. If a session hands you a
`claude/…` branch, rename it to `agent/…` before pushing. House style here,
pinned in [`claude.yml`](.github/workflows/claude.yml) via `branch_prefix: agent/`,
and hard-rejected by sibling repos' CI (meshcore-mobile).
<!-- invariant: conventional-commits -->
### Conventional commits for PR titles and commit subjects
`fix:`, `feat:`, `docs:`, `test:`, `ci:`, … PRs are squash-merged with
`squash_merge_commit_message=BLANK`, so the squashed commit carries the **PR title
only** — release-please reads that. The
[`PR Title`](.github/workflows/pr-title.yml) workflow checks it.
<!-- invariant: format-before-commit -->
### Run the formatter before committing
CI's `format` job runs `./gradlew ktfmtCheckAll` and it is a hard gate —
`ktfmtCheck` aborts on the first unformatted file. Before each commit touching
`*.kt` / `*.kts`, run `./gradlew ktfmtFormat` (or
`./gradlew :<module>:ktfmtFormatMain :<module>:ktfmtFormatTest` for the touched
modules) and stage the result. For the serve-web TypeScript, run
`npm --prefix cli/serve-web run format`. The round-trip through CI costs more
than running the formatter locally.
<!-- invariant: pr-state-recheck -->
### Re-check PR state immediately before **every** push
Not just the first commit of a session — a PR can merge between turns. Right
before pushing, `git fetch origin main` and confirm the branch head is not already
in `origin/main` (or read the PR's `state` / `merged`). If the PR has landed,
**stop**: do not stack commits onto the merged branch. Create a fresh branch from
`origin/main` for the follow-up and say so.
## PR workflow
The rules below are normative; the detail, the failure modes and the worked
examples are in [`docs/AGENT_GUIDE.md` → PR workflow](docs/AGENT_GUIDE.md#pr-workflow).
- **Open a PR automatically when a coding task is finished, committed and
pushed.** Conventional-commit title, short summary + test plan. Check first
whether the branch already has an open PR and push to it rather than opening a
duplicate. Skip only when the user explicitly said "no PR" or framed the task as
pure exploration.
- **A UI-affecting PR must carry before/after visual evidence as embedded,
viewable images** — markdown `![](…)` pointing at committed render PNGs
(commit-pinned `raw.githubusercontent.com` URLs) or images uploaded into the
body. Describing an image, or deferring to the visual-diff bot's auto-comment,
is not evidence. If a surface genuinely cannot be captured, say so and embed a
renderable proxy or state how a human/CI verifies it. Text-only is correct for
non-visual changes.
- **Write `![alt](url)` and leave the backticks alone if they appear.** They are
injected between the agent and GitHub, not authored, and the
[`PR Body Syntax`](.github/workflows/pr-body-syntax.yml) workflow repairs them
in place. Do not hand-fix or re-post to "correct" the syntax. The three cases
the repair does not cover are listed in `docs/AGENT_GUIDE.md`.
- **Wire new visual surfaces into the preview workflow** so the next change to
them is diffed without anyone remembering to do it.
- **Don't auto-merge your own PR.** Opening, tracking and fix-up commits are
automatic; pressing merge on a PR *you* opened is the user's call, and no agent
approves or merges one. This is about agent-opened PRs. It is not a blanket
ban on merge automation in the repository: Renovate's dependency PRs are
automerged by configuration (`platformAutomerge` in
[`.github/renovate.json`](.github/renovate.json)), which GitHub applies only
once every required check of the `Protect Main` ruleset is green. Do not
report that setting as a violation of this bullet.
## Where everything else lives
| You need | Read |
| --- | --- |
| Which repository a module belongs in | [`docs/design/REPOSITORY_LAYERS.md`](docs/design/REPOSITORY_LAYERS.md) |
| Architecture, commands, state seams, constraints | [`docs/AGENT_GUIDE.md`](docs/AGENT_GUIDE.md) |
+ | Why `settings.gradle.kts` is shaped the way it is | [`docs/build-scripts/SETTINGS.md`](docs/build-scripts/SETTINGS.md) |
| The contributor doc index | [`docs/README.md`](docs/README.md) |
| Releasing, versioning | [`docs/RELEASING.md`](docs/RELEASING.md), [`docs/VERSIONING.md`](docs/VERSIONING.md) |
| Invoking an agent from an issue or PR | [`docs/AGENT_INVOCATION.md`](docs/AGENT_INVOCATION.md) |
| Which agent reads which file, and what it costs | [`docs/AGENT_ENTRYPOINTS.md`](docs/AGENT_ENTRYPOINTS.md) |
| Consumer docs for the published plugin and CLI | [`yschimke/skills`](https://github.com/yschimke/skills) |
| The VS Code extension (split out; consumes this repo's published plugin) | [`yschimke/compose-preview-vscode`](https://github.com/yschimke/compose-preview-vscode) |
Claude Code sessions additionally get on-demand procedures under
[`.claude/skills/`](.claude/skills/) — `steward` (driving a PR to green),
`render-evidence` (capturing before/after renders), `flake-triage` (proving a
preview is unstable rather than regressed). They carry harness mechanics only and
cite this file for the rules.