AGENTS.md · diff

git:20260917.ba516ec to git:20261005.7e0f7c7

15 added, 1 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`). The renderers, the render daemons and
the data extractors themselves live in
[`yschimke/compose-preview-daemon`](https://github.com/yschimke/compose-preview-daemon)
and are consumed here at the `composeai-preview-daemon` pin. 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
+ **Human reviewers: run the detector before reporting an attribution finding.** Automated
+ reviewers do not report attribution at all; see [Review guidelines](#review-guidelines). 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, and the reports since have kept naming commits that are not objects in
this repository at all. 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, starting with whether the commit exists:
```
git cat-file -t <sha> # a commit you are about to name must actually exist
git log --format='%h %an <%ae> | %cn <%ce>' <base>..<head>
.github/scripts/agent-attribution-scan.sh --range '<base>..<head>'
```
+ `<head>` must be the pull request's head commit as GitHub reports it: a commit made in a review
+ checkout, for example by reapplying the diff, carries the reviewer's identity, and that is how
+ every false report so far arose.
+
`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.
- **Embed images only from a GitHub-hosted origin.** Claude Code on the web
silently rewrites `![alt](url)` to `[alt](url)` on the way to the API whenever
the destination is not a GitHub host, so the picture arrives as a bare link and
the call still succeeds. `raw.githubusercontent.com`,
`github.com/<owner>/<repo>/raw/<ref>/…`, `github.com/user-attachments/assets/…`
and the `user-images` / `private-user-images` / `avatars` / `objects` / `media`
/ `gist` `.githubusercontent.com` hosts survive; everything else — a preview
deployment, a shields.io badge, `camo.githubusercontent.com` — does not. Applies
to issue bodies and comments as much as PR descriptions. The measured host list,
the upstream issue, and why it is not worth routing around are in
[`docs/AGENT_GUIDE.md` → PR workflow](docs/AGENT_GUIDE.md#pr-workflow).
- **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.
+
+ ## Review guidelines
+
+ - **Do not report commit author, committer or `Co-authored-by:` findings.** Your review sandbox
+ applies the pull request as a new commit under your own identity (`Codex <codex@openai.com>`),
+ so any identity you read from a local `git log` is yours, not the author's. Every such finding
+ on these repositories has named a commit that exists only in the sandbox. Attribution is a
+ human and CI concern; leave it out of reviews entirely. This overrides the human reviewer
+ guidance on attribution elsewhere in this file.
## Running Gradle
Wrap Gradle in [`build-brief`](https://bb.staticvar.dev). It keeps the full log on disk and prints
only the parts that decide what you do next — failed tasks, failed tests, warnings, build scan URLs,
artifact paths — and it preserves Gradle's exit code exactly. This repository is one where that
matters: nothing here is CI-only, so agents run `check` and the full `composePreviewRenderAll`
render pipeline in-session, and those bury their one real line in thousands.
Install it once (`brew install static-var/tap/build-brief`, or the script installer); the setup, the
per-command detail and the cases where you still want the raw log are in
[`docs/AGENT_GUIDE.md` → Common commands](docs/AGENT_GUIDE.md#common-commands).
On a shared developer host, automated builds use [`scripts/agent-gradle.sh`](scripts/agent-gradle.sh)
instead of invoking `build-brief` directly:
```
scripts/agent-gradle.sh :gradle-plugin:test --tests '*DeviceDimensionsTest*'
scripts/agent-gradle.sh --exclusive check
```
The launcher keeps `build-brief` while limiting automation to four low-priority workers,
non-interactive input and a ten-minute Gradle-daemon idle timeout. Use the normal profile for focused
compilation, formatting, tests and renders. Use `--exclusive` for `check`, broad render pipelines and
other heavyweight task graphs: it takes the same per-user machine lock as the Compose Preview daemon
and server repositories, so automated builds cannot peak together. Direct Gradle and `build-brief`
invocations remain unrestricted for interactive development, and hosted CI keeps its runner's full
capacity. Do not copy these limits into `gradle.properties`.
The per-command rules live in the managed `build-brief` block at the end of this file;
`build-brief --install` regenerates it, so edit it there rather than by hand.
Wrapping changes nothing about the invariants above — `./gradlew ktfmtFormat` is still what the
formatter rule means, just run through `build-brief`.
## 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) |
| Releasing ACROSS the five repositories, in order | [`docs/design/CROSS_REPO_RELEASES.md`](docs/design/CROSS_REPO_RELEASES.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.
<!-- build-brief:instructions:start -->
## build-brief
- Prefer `build-brief gradle ...` for PATH Gradle and `build-brief ./gradlew ...` for the project wrapper.
- For chained shell commands, rewrite each Gradle segment individually, for example `build-brief gradle test && build-brief gradle check`.
- Use default `build-brief` output for routine Gradle work; it stays intentionally short on clean success cases.
- Use default `build-brief` output for report-style commands like `tasks`, `help`, `projects`, `dependencies`, and `dependencyInsight`; their report bodies are preserved.
- Use `build-brief gradle --stacktrace ...` or `build-brief ./gradlew --stacktrace ...` when you need Gradle stack traces.
- `build-brief` normalizes output-shaping flags like `--quiet`, `--warn`, `--warning-mode ...`, and `--console ...` so its reducer keeps working reliably.
- Let Gradle daemon reuse happen by default; `build-brief` strips explicit `--daemon` and `--no-daemon` overrides rather than forcing daemon-off behavior.
- Preserve the raw log path from `build-brief` output when handing build failures to another tool or agent.
<!-- build-brief:instructions:end -->