AGENTS.md · git:20260914.69de871 · 2026-09-14 · sha256 1a6c732427a10ee3
AGENTS.md git:20260914.69de871A
Immutable. This exact content never changes and is served at /api/v1/blob/1a6c732427a10ee3.
# 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 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>' ``` `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 `` 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 `` 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. ## 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) | | 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 -->