AGENTS.md · git:20260910.93e56ec · 2026-09-10 · sha256 7284c639d12b5238
AGENTS.md git:20260910.93e56ecA
Immutable. This exact content never changes and is served at /api/v1/blob/7284c639d12b5238.
# 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: regenerate-lockfiles --> ### Regenerate the lockfiles when you move a dependency Every published module commits a `gradle.lockfile`, and those pins are `strictly` constraints. A `gradle/libs.versions.toml` bump not matched by regenerated lock state therefore does not merely go unrecorded — it **fails to resolve**, with `Dependency version enforced by Dependency Locking`. After touching the catalog or a `build.gradle.kts` dependency block, run `./gradlew resolveAndLockAll --write-locks --no-configuration-cache` (the flag is load-bearing) and stage every `gradle.lockfile` it moves. [`Dependency Locks`](.github/workflows/dependency-locks.yml) does this for Renovate's branches, but it runs on `pull_request` only and regenerates nothing when the new artifacts have not reached Maven Central yet — so a bump can still land on `main` stale. Lock state is not bookkeeping: the release-train publish guard diffs it *instead of* the catalog, which is the only reason one dependency bump does not republish all 94 modules. Detail: [`docs/AGENT_GUIDE.md` → Dependency lockfiles](docs/AGENT_GUIDE.md#dependency-lockfiles). <!-- 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. ## 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.